chore: Delete numerous Livewire feature tests.

This commit is contained in:
Yoga Pangestu 2026-01-02 13:50:53 +07:00
parent f657a5273f
commit 374f77897a
46 changed files with 0 additions and 15276 deletions

View File

@ -1,78 +0,0 @@
<?php
use App\Livewire\Auth\ForgotPassword;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Password;
use Livewire\Livewire;
uses(RefreshDatabase::class);
/*
|--------------------------------------------------------------------------
| Helpers
|--------------------------------------------------------------------------
*/
function attemptSendResetLink(string $email)
{
return Livewire::test(ForgotPassword::class)
->set('form.email', $email)
->call('sendLink');
}
function mockPasswordResetStatus(string $status, string $email = 'user@example.com'): void
{
Password::shouldReceive('sendResetLink')
->once()
->with(['form.email' => $email])
->andReturn($status);
}
/*
|--------------------------------------------------------------------------
| Component Rendering
|--------------------------------------------------------------------------
*/
it('renders forgot password component successfully', function () {
Livewire::test(ForgotPassword::class)
->assertViewIs('livewire.auth.forgot-password')
->assertSet('form.email', '');
});
/*
|--------------------------------------------------------------------------
| Validation Rules
|--------------------------------------------------------------------------
*/
it('validates email field is required', function () {
Livewire::test(ForgotPassword::class)
->set('form.email', '')
->call('sendLink')
->assertHasErrors(['form.email' => 'required']);
});
/*
|--------------------------------------------------------------------------
| Send Reset Link
|--------------------------------------------------------------------------
*/
it('sends reset link successfully', function () {
$email = 'user@example.com';
mockPasswordResetStatus(Password::ResetLinkSent, $email);
attemptSendResetLink($email)
->assertHasNoErrors();
});
it('handles failure when sending reset link', function () {
$email = 'user@example.com';
mockPasswordResetStatus('passwords.user', $email);
attemptSendResetLink($email)
->assertHasNoErrors();
});

View File

@ -1,250 +0,0 @@
<?php
use App\Livewire\Auth\Login;
use App\Models\Employee;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::class);
beforeEach(function () {
collect(['Developer', 'Owner', 'Leader', 'Admin', 'Partner', 'Customer'])
->each(fn ($role) => Role::create(['name' => $role]));
$this->correctPassword = config('myconfig.password_default');
$this->wrongPassword = 'wrongpassword';
});
/*
|--------------------------------------------------------------------------
| Helpers
|--------------------------------------------------------------------------
*/
function makeUser(?string $role = null, bool $isActive = true): User
{
$factory = User::factory();
$factory = $isActive
? $factory->active()
: $factory->inactive();
$user = $factory
->has(Employee::factory()->male()->permanent())
->create();
if ($role) {
$user->assignRole($role);
}
return $user;
}
function attemptLogin(string $login, string $password)
{
return Livewire::test(Login::class)
->set('form.login', $login)
->set('form.password', $password)
->call('auth');
}
/*
|--------------------------------------------------------------------------
| Component Rendering & Mount
|--------------------------------------------------------------------------
*/
it('renders login component successfully', function () {
Livewire::test(Login::class)
->assertViewIs('livewire.auth.login')
->assertSet('form.login', '')
->assertSet('form.password', '');
});
it('mounts component without query parameters', function () {
Livewire::test(Login::class)
->assertSet('form.login', '')
->assertSet('form.password', '');
});
it('mounts component with message query parameter', function () {
$message = 'Test message';
Livewire::withQueryParams(['message' => $message])
->test(Login::class)
->assertSet('form.login', '')
->assertSet('form.password', '');
});
/*
|--------------------------------------------------------------------------
| Validation Rules
|--------------------------------------------------------------------------
*/
it('validates required fields', function () {
Livewire::test(Login::class)
->set('form.login', '')
->set('form.password', '')
->call('auth')
->assertHasErrors(['form.login', 'form.password']);
});
it('validates login field is required', function () {
Livewire::test(Login::class)
->set('form.login', '')
->set('form.password', 'password123')
->call('auth')
->assertHasErrors(['form.login']);
});
it('validates password field is required', function () {
Livewire::test(Login::class)
->set('form.login', 'testuser')
->set('form.password', '')
->call('auth')
->assertHasErrors(['form.password']);
});
/*
|--------------------------------------------------------------------------
| Authentication Failures
|--------------------------------------------------------------------------
*/
it('fails authentication with wrong credentials', function () {
attemptLogin('nonexistent@example.com', $this->correctPassword);
expect(auth()->check())->toBeFalse();
});
it('fails authentication with non-existent user', function () {
attemptLogin('nonexistent@example.com', $this->wrongPassword);
expect(auth()->check())->toBeFalse();
});
/*
|--------------------------------------------------------------------------
| Authentication Works (email & username)
|--------------------------------------------------------------------------
*/
it('successfully authenticates with email', function () {
$user = makeUser('Developer');
attemptLogin($user->email, $this->correctPassword)->assertRedirect('/studio/dashboard/overview');
expect(auth()->check())->toBeTrue();
expect(auth()->id())->toBe($user->id);
});
it('successfully authenticates with username', function () {
$user = makeUser('Developer');
attemptLogin($user->username, $this->correctPassword)->assertRedirect('/studio/dashboard/overview');
expect(auth()->check())->toBeTrue();
expect(auth()->id())->toBe($user->id);
});
/*
|--------------------------------------------------------------------------
| Prevent Inactive User Login
|--------------------------------------------------------------------------
*/
it('prevents login for inactive user', function () {
$user = makeUser('Developer', false);
attemptLogin($user->email, $this->correctPassword);
expect(auth()->check())->toBeFalse();
});
/*
|--------------------------------------------------------------------------
| Role Redirect Tests
|--------------------------------------------------------------------------
*/
it('redirects developer to studio dashboard', function () {
$user = makeUser('Developer');
attemptLogin($user->email, $this->correctPassword)->assertRedirect('/studio/dashboard/overview');
expect(auth()->check())->toBeTrue();
expect(auth()->id())->toBe($user->id);
});
it('redirects owner to studio dashboard', function () {
$user = makeUser('Owner');
attemptLogin($user->email, $this->correctPassword)->assertRedirect('/studio/dashboard/overview');
expect(auth()->check())->toBeTrue();
expect(auth()->id())->toBe($user->id);
});
it('redirects leader to studio dashboard', function () {
$user = makeUser('Leader');
attemptLogin($user->email, $this->correctPassword)->assertRedirect('/studio/dashboard/overview');
expect(auth()->check())->toBeTrue();
expect(auth()->id())->toBe($user->id);
});
it('redirects admin to studio dashboard', function () {
$user = makeUser('Admin');
attemptLogin($user->email, $this->correctPassword)->assertRedirect('/studio/dashboard/overview');
expect(auth()->check())->toBeTrue();
expect(auth()->id())->toBe($user->id);
});
it('redirects partner to partner overview', function () {
$user = makeUser('Partner');
attemptLogin($user->email, $this->correctPassword)->assertRedirect('/partner/overview');
expect(auth()->check())->toBeTrue();
expect(auth()->id())->toBe($user->id);
});
it('redirects customer to member overview', function () {
$user = makeUser('Customer');
attemptLogin($user->email, $this->correctPassword)->assertRedirect('/member/overview');
expect(auth()->check())->toBeTrue();
expect(auth()->id())->toBe($user->id);
});
/*
|--------------------------------------------------------------------------
| Edge Case
|--------------------------------------------------------------------------
*/
it('redirects user without role to login', function () {
$user = makeUser();
attemptLogin($user->email, $this->correctPassword)->assertRedirect('/login');
expect(auth()->check())->toBeTrue();
expect(auth()->id())->toBe($user->id);
});
it('regenerates session on successful login', function () {
$user = makeUser('Developer');
attemptLogin($user->email, $this->correctPassword);
expect(auth()->check())->toBeTrue();
expect(auth()->user()->id)->toBe($user->id);
});

View File

@ -1,417 +0,0 @@
<?php
use App\Enums\Gender;
use App\Enums\UserStatus;
use App\Livewire\Auth\Register;
use App\Models\Customer;
use App\Models\Employee;
use App\Models\Membership;
use App\Models\PointRecord;
use App\Models\ReferralCode;
use App\Models\ReferralUsage;
use App\Models\Tier;
use App\Models\User;
use Database\Seeders\TierSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;
use Livewire\Livewire;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::class);
beforeEach(function () {
collect(['Developer', 'Owner', 'Leader', 'Admin', 'Partner', 'Customer'])
->each(fn ($role) => Role::create(['name' => $role]));
$this->seed(TierSeeder::class);
Notification::fake();
});
/*
|--------------------------------------------------------------------------
| Helpers
|--------------------------------------------------------------------------
*/
function fillStep1(?string $email = null, ?string $username = null, ?string $password = null)
{
return Livewire::test(Register::class)
->set('form.email', $email ?? 'info.pangestuyoga@gmail.com')
->set('form.username', $username ?? 'pangestu')
->set('form.password', $password ?? 'Aggygga13@');
}
function fillStep2(?string $name = null, ?string $phoneNumber = null, $gender = null, ?string $referralCode = null)
{
$component = fillStep1()
->set('form.name', $name ?? 'Test User')
->set('form.phone_number', $phoneNumber ?? '0812 3456 789')
->set('form.gender', $gender ?? Gender::MALE->value);
if ($referralCode !== null) {
$component->set('form.referral_code', $referralCode);
}
return $component;
}
function attemptRegistration(?string $referralCode = null)
{
fillStep1()->call('nextStep');
return fillStep2(referralCode: $referralCode)
->call('register');
}
function makeReferralCode(): ReferralCode
{
$user = User::factory()
->active()
->has(Employee::factory()->male()->permanent())
->has(ReferralCode::factory())
->create();
$user->assignRole('Customer');
return $user->referralCode;
}
/*
|--------------------------------------------------------------------------
| Component Rendering & Mount
|--------------------------------------------------------------------------
*/
it('renders register component successfully', function () {
Livewire::test(Register::class)
->assertViewIs('livewire.auth.register')
->assertSet('currentStep', 1)
->assertSet('totalSteps', 2)
->assertSet('form.email', '')
->assertSet('form.username', '')
->assertSet('form.password', '');
});
it('mounts component with correct initial state', function () {
Livewire::test(Register::class)
->assertSet('currentStep', 1)
->assertSet('totalSteps', 2)
->assertCount('steps', 2)
->assertSet('steps.0.name', 'Kredensial')
->assertSet('steps.1.name', 'Informasi');
});
/*
|--------------------------------------------------------------------------
| Step 1 Validation - Required Fields
|--------------------------------------------------------------------------
*/
it('validates step 1 required fields', function () {
Livewire::test(Register::class)
->set('form.email', '')
->set('form.username', '')
->set('form.password', '')
->call('nextStep')
->assertHasErrors(['form.email', 'form.username', 'form.password']);
});
it('validates email field is required in step 1', function () {
fillStep1('')
->call('nextStep')
->assertHasErrors(['form.email']);
});
it('validates username field is required in step 1', function () {
fillStep1(username: '')
->call('nextStep')
->assertHasErrors(['form.username']);
});
it('validates password field is required in step 1', function () {
fillStep1(password: '')
->call('nextStep')
->assertHasErrors(['form.password']);
});
/*
|--------------------------------------------------------------------------
| Step 1 Validation - Email Rules
|--------------------------------------------------------------------------
*/
it('validates email format in step 1', function () {
fillStep1('invalid-email')
->call('nextStep')
->assertHasErrors(['form.email']);
});
it('validates email is unique in step 1', function () {
$user = User::factory()->create();
fillStep1($user->email)
->call('nextStep')
->assertHasErrors(['form.email']);
});
/*
|--------------------------------------------------------------------------
| Step 1 Validation - Username Rules
|--------------------------------------------------------------------------
*/
it('validates username is unique in step 1', function () {
$user = User::factory()->create();
fillStep1(username: $user->username)
->call('nextStep')
->assertHasErrors(['form.username']);
});
it('validates username is alpha numeric in step 1', function () {
fillStep1(username: 'test user!')
->call('nextStep')
->assertHasErrors(['form.username']);
});
it('validates username minimum length in step 1', function () {
fillStep1(username: 'test')
->call('nextStep')
->assertHasErrors(['form.username']);
});
it('validates username maximum length in step 1', function () {
fillStep1(username: str_repeat('a', 21))
->call('nextStep')
->assertHasErrors(['form.username']);
});
/*
|--------------------------------------------------------------------------
| Step 1 Validation - Password Rules
|--------------------------------------------------------------------------
*/
it('validates password minimum length in step 1', function () {
fillStep1(password: 'Pass1!')
->call('nextStep')
->assertHasErrors(['form.password']);
});
it('validates password contains uppercase and lowercase letters in step 1', function () {
fillStep1(password: 'password123!')
->call('nextStep')
->assertHasErrors(['form.password']);
});
it('validates password contains numbers in step 1', function () {
fillStep1(password: 'Password!')
->call('nextStep')
->assertHasErrors(['form.password']);
});
it('validates password contains symbols in step 1', function () {
fillStep1(password: 'Password123')
->call('nextStep')
->assertHasErrors(['form.password']);
});
/*
|--------------------------------------------------------------------------
| Step 1 Flow Control
|--------------------------------------------------------------------------
*/
it('moves to step 2 when step 1 validation passes', function () {
fillStep1()
->call('nextStep')
->assertHasNoErrors()
->assertSet('currentStep', 2);
});
/*
|--------------------------------------------------------------------------
| Step 2 Validation - Required Fields
|--------------------------------------------------------------------------
*/
it('validates step 2 required fields', function () {
fillStep1()
->set('form.name', '')
->set('form.phone_number', '')
->set('form.gender', '')
->call('register')
->assertHasErrors(['form.name', 'form.phone_number', 'form.gender']);
});
it('validates name field is required in step 2', function () {
fillStep2('')
->call('register')
->assertHasErrors(['form.name']);
});
it('validates phone number field is required in step 2', function () {
fillStep2(phoneNumber: '')
->call('register')
->assertHasErrors(['form.phone_number']);
});
it('validates gender field is required in step 2', function () {
fillStep2(gender: '')
->call('register')
->assertHasErrors(['form.gender']);
});
/*
|--------------------------------------------------------------------------
| Step 2 Validation - Field Format
|--------------------------------------------------------------------------
*/
it('validates phone number format in step 2', function () {
fillStep2(phoneNumber: '08123456789')
->call('register')
->assertHasErrors(['form.phone_number']);
});
it('validates gender is valid enum value in step 2', function () {
fillStep2()
->set('form.gender', 3)
->call('register')
->assertHasErrors(['form.gender']);
});
it('validates referral code exists when provided in step 2', function () {
fillStep2(referralCode: 'INVALIDCODE')
->call('register')
->assertHasErrors(['form.referral_code']);
});
/*
|--------------------------------------------------------------------------
| Step 2 Flow Control
|--------------------------------------------------------------------------
*/
it('can navigate back to step 1 from step 2', function () {
fillStep1()
->call('nextStep')
->assertSet('currentStep', 2)
->call('prevStep')
->assertSet('currentStep', 1);
});
it('cannot navigate before step 1', function () {
Livewire::test(Register::class)
->assertSet('currentStep', 1)
->call('prevStep')
->assertSet('currentStep', 1);
});
it('can update step directly', function () {
Livewire::test(Register::class)
->call('updateStep', 2)
->assertSet('currentStep', 1);
});
it('validates step 1 when updating step directly to step 2', function () {
Livewire::test(Register::class)
->set('form.email', '')
->set('form.username', '')
->set('form.password', '')
->call('updateStep', 2)
->assertHasErrors(['form.email', 'form.username', 'form.password'])
->assertSet('currentStep', 1);
});
/*
|--------------------------------------------------------------------------
| Registration Success Cases
|--------------------------------------------------------------------------
*/
it('successfully registers a new user without referral code', function () {
attemptRegistration()
->assertHasNoErrors()
->assertRedirect('/member/overview');
expect(User::where('email', 'info.pangestuyoga@gmail.com')->exists())->toBeTrue();
expect(Customer::whereHas('user', fn ($q) => $q->where('email', 'info.pangestuyoga@gmail.com'))->exists())->toBeTrue();
$user = User::where('email', 'info.pangestuyoga@gmail.com')->first();
expect($user->status)->toBe(UserStatus::ACTIVE);
expect($user->hasRole('Customer'))->toBeTrue();
expect(Membership::where('user_id', $user->id)->exists())->toBeTrue();
expect(ReferralCode::where('user_id', $user->id)->exists())->toBeTrue();
expect(auth()->check())->toBeTrue();
});
it('successfully registers a new user with referral code', function () {
$referralCode = makeReferralCode();
attemptRegistration($referralCode->code)
->assertHasNoErrors()
->assertRedirect('/member/overview');
$user = User::where('email', 'info.pangestuyoga@gmail.com')->first();
expect(ReferralUsage::where('user_id', $user->id)->exists())->toBeTrue();
$referralCode->refresh();
expect($referralCode->uses_count)->toBe(1);
});
/*
|--------------------------------------------------------------------------
| Registration Data Creation
|--------------------------------------------------------------------------
*/
it('creates membership with correct tier and initial data', function () {
attemptRegistration()
->assertHasNoErrors();
$user = User::where('email', 'info.pangestuyoga@gmail.com')->first();
$membership = Membership::where('user_id', $user->id)->first();
$tier = Tier::orderBy('min_spending')->first();
expect($membership->tier_id)->toBe($tier->id);
expect($membership->total_spending)->toBe(0);
expect($membership->reward_points)->toBe(10);
});
it('creates point record for registration', function () {
attemptRegistration()
->assertHasNoErrors();
$user = User::where('email', 'info.pangestuyoga@gmail.com')->first();
$pointRecord = PointRecord::where('user_id', $user->id)->first();
expect($pointRecord->description)->toBe('Pendaftaran');
expect($pointRecord->change)->toBe(10);
expect($pointRecord->is_addition)->toBeTrue();
});
/*
|--------------------------------------------------------------------------
| Real-time Validation
|--------------------------------------------------------------------------
*/
it('validates individual field on update for step 1', function () {
Livewire::test(Register::class)
->set('form.email', 'invalid-email')
->assertHasErrors(['form.email'])
->set('form.email', 'valid@example.com')
->assertHasNoErrors(['form.email']);
});
it('validates individual field on update for step 2', function () {
fillStep1()->call('nextStep')
->set('form.phone_number', 'invalid')
->assertHasErrors(['form.phone_number'])
->set('form.phone_number', '0812 3456 789')
->assertHasNoErrors(['form.phone_number']);
});

View File

@ -1,102 +0,0 @@
<?php
use App\Livewire\Auth\ResetPassword;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Password;
use Livewire\Livewire;
uses(RefreshDatabase::class);
/*
|--------------------------------------------------------------------------
| Helpers
|--------------------------------------------------------------------------
*/
function makeResetComponent(string $token = 'sample-token', string $email = 'user@example.com')
{
return Livewire::withQueryParams([
'token' => $token,
'email' => $email,
])->test(ResetPassword::class);
}
function attemptResetPassword(
string $token = 'sample-token',
string $email = 'user@example.com',
string $password = 'password123',
string $passwordConfirmation = 'password123'
) {
return makeResetComponent($token, $email)
->set('form.password', $password)
->set('form.password_confirmation', $passwordConfirmation)
->call('resetPassword');
}
function mockPasswordReset(string $status): void
{
Password::shouldReceive('reset')
->once()
->andReturn($status);
}
/*
|--------------------------------------------------------------------------
| Component Rendering & Mount
|--------------------------------------------------------------------------
*/
it('renders reset password component successfully', function () {
$token = 'sample-token';
$email = 'user@example.com';
makeResetComponent($token, $email)
->assertViewIs('livewire.auth.reset-password')
->assertSet('token', $token)
->assertSet('email', $email);
});
/*
|--------------------------------------------------------------------------
| Validation Rules
|--------------------------------------------------------------------------
*/
it('validates password fields are required', function () {
makeResetComponent()
->set('form.password', '')
->set('form.password_confirmation', '')
->call('resetPassword')
->assertHasErrors(['form.password', 'form.password_confirmation']);
});
it('validates password confirmation must match', function () {
makeResetComponent()
->set('form.password', 'password123')
->set('form.password_confirmation', 'different123')
->call('resetPassword')
->assertHasErrors(['form.password' => 'confirmed']);
});
/*
|--------------------------------------------------------------------------
| Reset Password
|--------------------------------------------------------------------------
*/
it('resets password successfully', function () {
mockPasswordReset(Password::PasswordReset);
attemptResetPassword()
->assertHasNoErrors()
->assertRedirect(route('login', [
'message' => 'Kata sandi berhasil diperbarui, silakan masuk kembali.',
]));
});
it('handles failure when resetting password', function () {
mockPasswordReset('passwords.token');
attemptResetPassword()
->assertHasNoErrors();
});

View File

@ -1,466 +0,0 @@
<?php
use App\Enums\AuditStockStatus;
use App\Models\Bottle;
use App\Models\Employee;
use App\Models\Outlet;
use App\Models\User;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Spatie\Activitylog\Models\Activity;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::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);
// Create test outlets
$this->outlets = Outlet::factory()->count(3)->create();
});
function mountBottleAuditComponent(User $user, Outlet $outlet, Bottle $bottle)
{
return Livewire::actingAs($user)->test(\App\Livewire\Studio\Catalog\Bottle\Audit::class, ['outlet' => $outlet, 'bottle' => $bottle]);
}
/*
|--------------------------------------------------------------------------
| Audit Component Rendering & Mount
|--------------------------------------------------------------------------
*/
it('bottle audit component initializes correctly', function () {
$bottle = Bottle::factory()->create(['name' => 'Test Bottle']);
$outlet = $this->outlets->first();
$component = mountBottleAuditComponent($this->user, $outlet, $bottle);
expect($component->label)->toBe('Test Bottle di '.$outlet->name);
expect($component->activityLogs)->toBeInstanceOf(\Illuminate\Database\Eloquent\Collection::class);
});
/*
|--------------------------------------------------------------------------
| Activity Logs Display Tests
|--------------------------------------------------------------------------
*/
it('displays bottle activity logs correctly', function () {
$bottle = Bottle::factory()->create();
$outlet = $this->outlets->first();
// Create some activity logs with expected properties
Activity::create([
'log_name' => 'default',
'description' => 'Stock updated',
'subject_type' => get_class($bottle),
'subject_id' => $bottle->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 5,
'quantity_change' => 10,
'new_stock' => 15,
],
'event' => AuditStockStatus::INCREASED->value,
]);
Activity::create([
'log_name' => 'default',
'description' => 'Stock decreased',
'subject_type' => get_class($bottle),
'subject_id' => $bottle->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 15,
'quantity_change' => -5,
'new_stock' => 10,
],
'event' => AuditStockStatus::DECREASED->value,
]);
$component = mountBottleAuditComponent($this->user, $outlet, $bottle);
expect($component->activityLogs)->toHaveCount(2);
expect($component->activityLogs->first()->event['label'])->toBe(AuditStockStatus::INCREASED->label());
expect($component->activityLogs->first()->event['color'])->toBe(AuditStockStatus::INCREASED->color());
expect($component->activityLogs->last()->event['label'])->toBe(AuditStockStatus::DECREASED->label());
expect($component->activityLogs->last()->event['color'])->toBe(AuditStockStatus::DECREASED->color());
});
it('filters bottle activity logs by outlet', function () {
$bottle = Bottle::factory()->create();
$outlet1 = $this->outlets->first();
$outlet2 = $this->outlets->last();
// Create activity for outlet1
Activity::create([
'log_name' => 'default',
'description' => 'Stock updated outlet1',
'subject_type' => get_class($bottle),
'subject_id' => $bottle->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet1->id,
'previous_stock' => 5,
'quantity_change' => 10,
'new_stock' => 15,
],
'event' => AuditStockStatus::UPDATED->value,
]);
// Create activity for outlet2
Activity::create([
'log_name' => 'default',
'description' => 'Stock increased outlet2',
'subject_type' => get_class($bottle),
'subject_id' => $bottle->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet2->id,
'previous_stock' => 0,
'quantity_change' => 20,
'new_stock' => 20,
],
'event' => AuditStockStatus::INCREASED->value,
]);
$component1 = mountBottleAuditComponent($this->user, $outlet1, $bottle);
$component2 = mountBottleAuditComponent($this->user, $outlet2, $bottle);
expect($component1->activityLogs)->toHaveCount(1);
expect($component1->activityLogs->first()->description)->toBe('Stock updated outlet1');
expect($component2->activityLogs)->toHaveCount(1);
expect($component2->activityLogs->first()->description)->toBe('Stock increased outlet2');
});
it('orders bottle activity logs by latest first', function () {
$bottle = Bottle::factory()->create();
$outlet = $this->outlets->first();
// Create activities with different timestamps
$activity1 = Activity::create([
'log_name' => 'default',
'description' => 'first activity',
'subject_type' => get_class($bottle),
'subject_id' => $bottle->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 5,
'quantity_change' => 5,
'new_stock' => 10,
],
'event' => AuditStockStatus::UPDATED->value,
'created_at' => now()->subMinutes(5),
]);
$activity2 = Activity::create([
'log_name' => 'default',
'description' => 'second activity',
'subject_type' => get_class($bottle),
'subject_id' => $bottle->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 10,
'quantity_change' => 10,
'new_stock' => 20,
],
'event' => AuditStockStatus::INCREASED->value,
'created_at' => now(),
]);
$component = mountBottleAuditComponent($this->user, $outlet, $bottle);
expect($component->activityLogs)->toHaveCount(2);
expect($component->activityLogs->first()->description)->toBe('second activity');
expect($component->activityLogs->last()->description)->toBe('first activity');
});
/*
|--------------------------------------------------------------------------
| Activity Logs Content Tests
|--------------------------------------------------------------------------
*/
it('maps bottle activity events to correct labels and colors', function () {
$bottle = Bottle::factory()->create();
$outlet = $this->outlets->first();
// Test INCREASED event
Activity::create([
'log_name' => 'default',
'description' => 'Stock increased',
'subject_type' => get_class($bottle),
'subject_id' => $bottle->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 10,
'quantity_change' => 5,
'new_stock' => 15,
],
'event' => AuditStockStatus::INCREASED->value,
]);
// Test DECREASED event
Activity::create([
'log_name' => 'default',
'description' => 'Stock decreased',
'subject_type' => get_class($bottle),
'subject_id' => $bottle->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 15,
'quantity_change' => -3,
'new_stock' => 12,
],
'event' => AuditStockStatus::DECREASED->value,
]);
// Test UPDATED event
Activity::create([
'log_name' => 'default',
'description' => 'Stock updated',
'subject_type' => get_class($bottle),
'subject_id' => $bottle->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 12,
'quantity_change' => 0,
'new_stock' => 12,
],
'event' => AuditStockStatus::UPDATED->value,
]);
$component = mountBottleAuditComponent($this->user, $outlet, $bottle);
expect($component->activityLogs)->toHaveCount(3);
// Check INCREASED event mapping
$increasedLog = collect($component->activityLogs)->firstWhere('event.label', AuditStockStatus::INCREASED->label());
expect($increasedLog['event']['color'])->toBe(AuditStockStatus::INCREASED->color());
// Check DECREASED event mapping
$decreasedLog = collect($component->activityLogs)->firstWhere('event.label', AuditStockStatus::DECREASED->label());
expect($decreasedLog['event']['color'])->toBe(AuditStockStatus::DECREASED->color());
// Check UPDATED event mapping
$updatedLog = collect($component->activityLogs)->firstWhere('event.label', AuditStockStatus::UPDATED->label());
expect($updatedLog['event']['color'])->toBe(AuditStockStatus::UPDATED->color());
});
/*
|--------------------------------------------------------------------------
| Empty State Tests
|--------------------------------------------------------------------------
*/
it('handles empty bottle activity logs in audit component', function () {
$bottle = Bottle::factory()->create();
$outlet = $this->outlets->first();
$component = mountBottleAuditComponent($this->user, $outlet, $bottle);
expect($component->activityLogs)->toBeInstanceOf(\Illuminate\Database\Eloquent\Collection::class);
expect($component->activityLogs)->toHaveCount(0);
});
it('handles bottle with no activities in specific outlet', function () {
$bottle = Bottle::factory()->create();
$outlet1 = $this->outlets->first();
$outlet2 = $this->outlets->last();
// Create activity only for outlet2
Activity::create([
'log_name' => 'default',
'description' => 'Stock updated',
'subject_type' => get_class($bottle),
'subject_id' => $bottle->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet2->id,
'previous_stock' => 5,
'quantity_change' => 5,
'new_stock' => 10,
],
'event' => AuditStockStatus::UPDATED->value,
]);
$component1 = mountBottleAuditComponent($this->user, $outlet1, $bottle);
$component2 = mountBottleAuditComponent($this->user, $outlet2, $bottle);
expect($component1->activityLogs)->toHaveCount(0);
expect($component2->activityLogs)->toHaveCount(1);
});
/*
|--------------------------------------------------------------------------
| Integration Tests
|--------------------------------------------------------------------------
*/
it('displays real bottle activity log data correctly', function () {
$bottle = Bottle::factory()->create(['name' => 'Integration Bottle']);
$outlet = $this->outlets->first();
$activity = Activity::create([
'log_name' => 'default',
'description' => 'Stock adjustment for integration test',
'subject_type' => get_class($bottle),
'subject_id' => $bottle->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 100,
'quantity_change' => 25,
'new_stock' => 125,
],
'event' => AuditStockStatus::INCREASED->value,
]);
$component = mountBottleAuditComponent($this->user, $outlet, $bottle);
expect($component->activityLogs)->toHaveCount(1);
$log = $component->activityLogs->first();
expect($log->description)->toBe('Stock adjustment for integration test');
expect($log->properties['previous_stock'])->toBe(100);
expect($log->properties['quantity_change'])->toBe(25);
expect($log->properties['new_stock'])->toBe(125);
expect($log->event['label'])->toBe(AuditStockStatus::INCREASED->label());
expect($log->event['color'])->toBe(AuditStockStatus::INCREASED->color());
});
it('handles multiple bottle activities across different outlets', function () {
$bottle = Bottle::factory()->create();
$outlet1 = $this->outlets->first();
$outlet2 = $this->outlets->last();
// Create activities for both outlets
Activity::create([
'log_name' => 'default',
'description' => 'Activity in outlet 1',
'subject_type' => get_class($bottle),
'subject_id' => $bottle->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet1->id,
'previous_stock' => 10,
'quantity_change' => 5,
'new_stock' => 15,
],
'event' => AuditStockStatus::INCREASED->value,
]);
Activity::create([
'log_name' => 'default',
'description' => 'Activity in outlet 2',
'subject_type' => get_class($bottle),
'subject_id' => $bottle->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet2->id,
'previous_stock' => 20,
'quantity_change' => -3,
'new_stock' => 17,
],
'event' => AuditStockStatus::DECREASED->value,
]);
$component1 = mountBottleAuditComponent($this->user, $outlet1, $bottle);
$component2 = mountBottleAuditComponent($this->user, $outlet2, $bottle);
expect($component1->activityLogs)->toHaveCount(1);
expect($component1->activityLogs->first()->description)->toBe('Activity in outlet 1');
expect($component2->activityLogs)->toHaveCount(1);
expect($component2->activityLogs->first()->description)->toBe('Activity in outlet 2');
});
/*
|--------------------------------------------------------------------------
| Edge Cases and Error Handling
|--------------------------------------------------------------------------
*/
it('handles bottle activity logs with missing properties gracefully', function () {
$bottle = Bottle::factory()->create();
$outlet = $this->outlets->first();
// Create activity with incomplete properties (shouldn't happen in real usage)
Activity::create([
'log_name' => 'default',
'description' => 'Incomplete activity',
'subject_type' => get_class($bottle),
'subject_id' => $bottle->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 0, // Add required properties
'quantity_change' => 0,
'new_stock' => 0,
],
'event' => AuditStockStatus::UPDATED->value,
]);
$component = mountBottleAuditComponent($this->user, $outlet, $bottle);
expect($component->activityLogs)->toHaveCount(1);
// Component should render correctly with all required properties
});
it('handles bottle activities with null causer', function () {
$bottle = Bottle::factory()->create();
$outlet = $this->outlets->first();
Activity::create([
'log_name' => 'default',
'description' => 'Activity with no causer',
'subject_type' => get_class($bottle),
'subject_id' => $bottle->id,
'causer_type' => null,
'causer_id' => null,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 5,
'quantity_change' => 5,
'new_stock' => 10,
],
'event' => AuditStockStatus::INCREASED->value,
]);
$component = mountBottleAuditComponent($this->user, $outlet, $bottle);
expect($component->activityLogs)->toHaveCount(1);
expect($component->activityLogs->first()->causer)->toBeNull();
});

View File

@ -1,344 +0,0 @@
<?php
use App\Models\Bottle;
use App\Models\Employee;
use App\Models\Outlet;
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);
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);
// Create test outlets
$this->outlets = Outlet::factory()->count(3)->create();
});
function mountBottleCreateComponent(User $user)
{
return Livewire::actingAs($user)->test(\App\Livewire\Studio\Catalog\Bottle\Create::class);
}
/*
|--------------------------------------------------------------------------
| Create Component Rendering & Mount
|--------------------------------------------------------------------------
*/
it('renders bottle create page successfully', function () {
mountBottleCreateComponent($this->user)
->assertViewIs('livewire.studio.catalog.bottle.form')
->assertViewHas('pageTitle', 'Tambah Botol');
});
it('initializes bottle form properties on mount', function () {
$component = mountBottleCreateComponent($this->user);
expect($component->form->name)->toBe('');
expect($component->form->size)->toBe('');
expect($component->form->cost_price)->toBe('');
expect($component->form->sale_price)->toBe('');
expect($component->form->description)->toBeNull();
expect($component->form->outlet_ids)->toBe([]);
expect($component->form->image)->toBe([]);
expect($component->outlets)->toHaveCount(3);
});
/*
|--------------------------------------------------------------------------
| Create Bottle Tests
|--------------------------------------------------------------------------
*/
it('creates bottle successfully when authorized', function () {
$data = [
'name' => 'New Bottle',
'size' => '100',
'cost_price' => '50000',
'sale_price' => '75000',
'description' => 'Bottle description',
'outlet_ids' => [$this->outlets->first()->id, $this->outlets->last()->id],
];
mountBottleCreateComponent($this->user)
->set('form.name', $data['name'])
->set('form.size', $data['size'])
->set('form.cost_price', $data['cost_price'])
->set('form.sale_price', $data['sale_price'])
->set('form.description', $data['description'])
->set('form.outlet_ids', $data['outlet_ids'])
->call('save')
->assertHasNoErrors()
->assertRedirect(route('studio.catalog.bottle.index'));
expect(Bottle::count())->toBe(1);
$bottle = Bottle::first();
expect($bottle->name)->toBe($data['name']);
expect($bottle->size)->toBe(100);
expect($bottle->cost_price)->toBe(50000);
expect($bottle->sale_price)->toBe(75000);
expect($bottle->description)->toBe($data['description']);
expect($bottle->outlets)->toHaveCount(2);
});
it('prevents bottle creation when unauthorized', function () {
$this->ownerRole->revokePermissionTo('create bottle');
Gate::define('create bottle', fn () => false);
mountBottleCreateComponent($this->user)
->set('form.name', 'Test Bottle')
->set('form.size', '50')
->set('form.sale_price', '25000')
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertForbidden();
});
it('creates bottle with empty cost_price for non-owner roles', function () {
// Create user with limited role
$this->user->roles()->detach();
$limitedRole = Role::create(['name' => 'Cashier']);
$this->user->roles()->attach($limitedRole->id);
$createPermission = Permission::where('name', 'create bottle')->first();
$limitedRole->givePermissionTo($createPermission);
mountBottleCreateComponent($this->user)
->set('form.name', 'Test Bottle')
->set('form.size', '30')
->set('form.cost_price', '') // Empty cost price
->set('form.sale_price', '20000')
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasNoErrors();
$bottle = Bottle::first();
expect($bottle->cost_price)->toBe(0);
});
/*
|--------------------------------------------------------------------------
| Form Validation Tests
|--------------------------------------------------------------------------
*/
it('validates required name field on bottle create', function () {
mountBottleCreateComponent($this->user)
->set('form.name', '')
->set('form.size', '100')
->set('form.sale_price', '50000')
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasErrors(['form.name']);
});
it('validates name field max length on bottle create', function () {
mountBottleCreateComponent($this->user)
->set('form.name', str_repeat('a', 51))
->set('form.size', '100')
->set('form.sale_price', '50000')
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasErrors(['form.name']);
});
it('validates required size field on bottle create', function () {
mountBottleCreateComponent($this->user)
->set('form.name', 'Test Bottle')
->set('form.size', '')
->set('form.sale_price', '50000')
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasErrors(['form.size']);
});
it('validates size is unsigned integer on bottle create', function () {
mountBottleCreateComponent($this->user)
->set('form.name', 'Test Bottle')
->set('form.size', '-50') // Negative value
->set('form.sale_price', '50000')
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasErrors(['form.size']);
});
it('validates required sale_price field on bottle create', function () {
mountBottleCreateComponent($this->user)
->set('form.name', 'Test Bottle')
->set('form.size', '100')
->set('form.sale_price', '')
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasErrors(['form.sale_price']);
});
it('validates sale_price is unsigned integer on bottle create', function () {
mountBottleCreateComponent($this->user)
->set('form.name', 'Test Bottle')
->set('form.size', '100')
->set('form.sale_price', '-50000') // Negative value
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasErrors(['form.sale_price']);
});
it('validates required outlet_ids field on bottle create', function () {
mountBottleCreateComponent($this->user)
->set('form.name', 'Test Bottle')
->set('form.size', '100')
->set('form.sale_price', '50000')
->set('form.outlet_ids', [])
->call('save')
->assertHasErrors(['form.outlet_ids']);
});
it('validates outlet_ids contains valid outlet ids on bottle create', function () {
mountBottleCreateComponent($this->user)
->set('form.name', 'Test Bottle')
->set('form.size', '100')
->set('form.sale_price', '50000')
->set('form.outlet_ids', [999])
->call('save')
->assertHasErrors(['form.outlet_ids.*']);
});
/*
|--------------------------------------------------------------------------
| Real-time Validation Tests
|--------------------------------------------------------------------------
*/
it('validates bottle name field in real-time', function () {
$component = mountBottleCreateComponent($this->user);
// Valid name
$component->set('form.name', 'Valid Bottle Name')
->assertHasNoErrors(['form.name']);
// Empty name
$component->set('form.name', '')
->assertHasErrors(['form.name']);
// Name too long
$component->set('form.name', str_repeat('a', 51))
->assertHasErrors(['form.name']);
});
it('validates bottle size field in real-time', function () {
$component = mountBottleCreateComponent($this->user);
// Valid size
$component->set('form.size', '100')
->assertHasNoErrors(['form.size']);
// Empty size
$component->set('form.size', '')
->assertHasErrors(['form.size']);
// Negative size
$component->set('form.size', '-50')
->assertHasErrors(['form.size']);
});
it('validates bottle price fields in real-time', function () {
$component = mountBottleCreateComponent($this->user);
// Valid sale price
$component->set('form.sale_price', '50000')
->assertHasNoErrors(['form.sale_price']);
// Empty sale price
$component->set('form.sale_price', '')
->assertHasErrors(['form.sale_price']);
// Negative sale price
$component->set('form.sale_price', '-50000')
->assertHasErrors(['form.sale_price']);
// Valid cost price
$component->set('form.cost_price', '30000')
->assertHasNoErrors(['form.cost_price']);
// Negative cost price
$component->set('form.cost_price', '-30000')
->assertHasErrors(['form.cost_price']);
});
it('validates bottle outlet_ids in real-time', function () {
$component = mountBottleCreateComponent($this->user);
// Valid outlet IDs
$component->set('form.name', 'Test Bottle')
->set('form.size', '100')
->set('form.sale_price', '50000')
->set('form.outlet_ids', [$this->outlets->first()->id])
->assertHasNoErrors(['form.outlet_ids']);
// Empty outlet IDs - should trigger validation on submit
$component->set('form.outlet_ids', [])
->call('save')
->assertHasErrors(['form.outlet_ids']);
// Invalid outlet ID
$component = mountBottleCreateComponent($this->user);
$component->set('form.name', 'Test Bottle')
->set('form.size', '100')
->set('form.sale_price', '50000')
->set('form.outlet_ids', [999])
->call('save')
->assertHasErrors(['form.outlet_ids.*']);
});
/*
|--------------------------------------------------------------------------
| Integration Tests
|--------------------------------------------------------------------------
*/
it('creates bottle and attaches to outlets correctly', function () {
$outlet1 = $this->outlets->first();
$outlet2 = $this->outlets->last();
mountBottleCreateComponent($this->user)
->set('form.name', 'Integration Test Bottle')
->set('form.size', '200')
->set('form.cost_price', '75000') // Required for Owner role
->set('form.sale_price', '100000')
->set('form.outlet_ids', [$outlet1->id, $outlet2->id])
->call('save')
->assertHasNoErrors();
$bottle = Bottle::where('size', 200)->first();
expect($bottle)->not->toBeNull();
expect($bottle->outlets)->toHaveCount(2);
expect($bottle->outlets->pluck('id')->toArray())->toContain($outlet1->id, $outlet2->id);
});
it('handles large numbers in bottle price fields', function () {
mountBottleCreateComponent($this->user)
->set('form.name', 'Expensive Bottle')
->set('form.size', '500')
->set('form.cost_price', '5000000') // 5 million
->set('form.sale_price', '7500000') // 7.5 million
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasNoErrors();
$bottle = Bottle::where('size', 500)->first();
expect($bottle->cost_price)->toBe(5000000);
expect($bottle->sale_price)->toBe(7500000);
});

View File

@ -1,344 +0,0 @@
<?php
use App\Models\Bottle;
use App\Models\Employee;
use App\Models\Outlet;
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);
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);
// Create test outlets
$this->outlets = Outlet::factory()->count(3)->create();
});
function mountBottleEditComponent(User $user, Bottle $bottle)
{
return Livewire::actingAs($user)->test(\App\Livewire\Studio\Catalog\Bottle\Edit::class, ['bottle' => $bottle->hash]);
}
/*
|--------------------------------------------------------------------------
| Edit Component Rendering & Mount
|--------------------------------------------------------------------------
*/
it('renders bottle edit page successfully', function () {
$bottle = Bottle::factory()->create();
mountBottleEditComponent($this->user, $bottle)
->assertViewIs('livewire.studio.catalog.bottle.form')
->assertViewHas('pageTitle', 'Ubah Botol');
});
it('populates bottle form with bottle data on mount', function () {
$bottle = Bottle::factory()->create([
'name' => 'Test Bottle',
'size' => 100,
'cost_price' => 50000,
'sale_price' => 75000,
'description' => 'Test description',
]);
$bottle->outlets()->attach([$this->outlets->first()->id]);
$component = mountBottleEditComponent($this->user, $bottle);
expect($component->form->name)->toBe('Test Bottle');
expect($component->form->size)->toBe('100');
expect($component->form->cost_price)->toBe('50000');
expect($component->form->sale_price)->toBe('75000');
expect($component->form->description)->toBe('Test description');
expect($component->form->outlet_ids)->toBe([$this->outlets->first()->id]);
});
/*
|--------------------------------------------------------------------------
| Update Bottle Tests
|--------------------------------------------------------------------------
*/
it('updates bottle successfully when authorized', function () {
$updatePermission = Permission::where('name', 'update bottle')->first();
$this->user->givePermissionTo($updatePermission);
$bottle = Bottle::factory()->create([
'name' => 'Original Bottle',
'size' => 50,
'cost_price' => 25000,
'sale_price' => 50000,
]);
$bottle->outlets()->attach([$this->outlets->first()->id]);
$updateData = [
'name' => 'Updated Bottle',
'size' => '100',
'cost_price' => '40000',
'sale_price' => '60000',
'description' => 'Updated description',
'outlet_ids' => [$this->outlets->first()->id, $this->outlets->last()->id],
];
mountBottleEditComponent($this->user, $bottle)
->set('form.name', $updateData['name'])
->set('form.size', $updateData['size'])
->set('form.cost_price', $updateData['cost_price'])
->set('form.sale_price', $updateData['sale_price'])
->set('form.description', $updateData['description'])
->set('form.outlet_ids', $updateData['outlet_ids'])
->call('save')
->assertHasNoErrors()
->assertRedirect(route('studio.catalog.bottle.index'));
$bottle->refresh();
expect($bottle->name)->toBe($updateData['name']);
expect($bottle->size)->toBe(100);
expect($bottle->cost_price)->toBe(40000);
expect($bottle->sale_price)->toBe(60000);
expect($bottle->description)->toBe($updateData['description']);
expect($bottle->outlets)->toHaveCount(2);
});
it('prevents bottle update when unauthorized', function () {
$this->ownerRole->revokePermissionTo('update bottle');
Gate::define('update bottle', fn () => false);
$bottle = Bottle::factory()->create();
mountBottleEditComponent($this->user, $bottle)
->set('form.name', 'Updated Name')
->call('save')
->assertForbidden();
});
it('syncs bottle outlet relationships correctly on update', function () {
$updatePermission = Permission::where('name', 'update bottle')->first();
$this->user->givePermissionTo($updatePermission);
$bottle = Bottle::factory()->create();
$bottle->outlets()->attach([$this->outlets->first()->id]);
// Change to different outlets
mountBottleEditComponent($this->user, $bottle)
->set('form.name', 'Updated Bottle')
->set('form.size', '200')
->set('form.sale_price', '50000')
->set('form.outlet_ids', [$this->outlets[1]->id, $this->outlets[2]->id])
->call('save')
->assertHasNoErrors();
$bottle->refresh();
expect($bottle->outlets)->toHaveCount(2);
expect($bottle->outlets->pluck('id')->toArray())->toBe([$this->outlets[1]->id, $this->outlets[2]->id]);
});
/*
|--------------------------------------------------------------------------
| Form Validation Tests on Edit
|--------------------------------------------------------------------------
*/
it('validates required name field on bottle update', function () {
$bottle = Bottle::factory()->create();
mountBottleEditComponent($this->user, $bottle)
->set('form.name', '')
->call('save')
->assertHasErrors(['form.name']);
});
it('validates name field max length on bottle update', function () {
$bottle = Bottle::factory()->create();
mountBottleEditComponent($this->user, $bottle)
->set('form.name', str_repeat('a', 51))
->call('save')
->assertHasErrors(['form.name']);
});
it('validates required size field on bottle update', function () {
$bottle = Bottle::factory()->create();
mountBottleEditComponent($this->user, $bottle)
->set('form.size', '')
->call('save')
->assertHasErrors(['form.size']);
});
it('validates size is unsigned integer on bottle update', function () {
$bottle = Bottle::factory()->create();
mountBottleEditComponent($this->user, $bottle)
->set('form.size', '-100')
->call('save')
->assertHasErrors(['form.size']);
});
it('validates required sale_price field on bottle update', function () {
$bottle = Bottle::factory()->create();
mountBottleEditComponent($this->user, $bottle)
->set('form.sale_price', '')
->call('save')
->assertHasErrors(['form.sale_price']);
});
it('validates sale_price is unsigned integer on bottle update', function () {
$bottle = Bottle::factory()->create();
mountBottleEditComponent($this->user, $bottle)
->set('form.sale_price', '-50000')
->call('save')
->assertHasErrors(['form.sale_price']);
});
it('validates required outlet_ids field on bottle update', function () {
$bottle = Bottle::factory()->create();
mountBottleEditComponent($this->user, $bottle)
->set('form.outlet_ids', [])
->call('save')
->assertHasErrors(['form.outlet_ids']);
});
it('validates outlet_ids contains valid outlet ids on bottle update', function () {
$bottle = Bottle::factory()->create();
mountBottleEditComponent($this->user, $bottle)
->set('form.outlet_ids', [999])
->call('save')
->assertHasErrors(['form.outlet_ids.*']);
});
/*
|--------------------------------------------------------------------------
| Real-time Validation Tests on Edit
|--------------------------------------------------------------------------
*/
it('validates bottle name field in real-time on edit', function () {
$bottle = Bottle::factory()->create();
$component = mountBottleEditComponent($this->user, $bottle);
// Valid name
$component->set('form.name', 'Valid Updated Bottle Name')
->assertHasNoErrors(['form.name']);
// Empty name
$component->set('form.name', '')
->assertHasErrors(['form.name']);
// Name too long
$component->set('form.name', str_repeat('a', 51))
->assertHasErrors(['form.name']);
});
it('validates bottle size field in real-time on edit', function () {
$bottle = Bottle::factory()->create();
$component = mountBottleEditComponent($this->user, $bottle);
// Valid size
$component->set('form.size', '150')
->assertHasNoErrors(['form.size']);
// Empty size
$component->set('form.size', '')
->assertHasErrors(['form.size']);
// Negative size
$component->set('form.size', '-50')
->assertHasErrors(['form.size']);
});
it('validates bottle price fields in real-time on edit', function () {
$bottle = Bottle::factory()->create();
$component = mountBottleEditComponent($this->user, $bottle);
// Valid sale price
$component->set('form.sale_price', '75000')
->assertHasNoErrors(['form.sale_price']);
// Empty sale price
$component->set('form.sale_price', '')
->assertHasErrors(['form.sale_price']);
// Negative sale price
$component->set('form.sale_price', '-75000')
->assertHasErrors(['form.sale_price']);
});
/*
|--------------------------------------------------------------------------
| Integration Tests
|--------------------------------------------------------------------------
*/
it('updates bottle and syncs outlet relationships', function () {
$updatePermission = Permission::where('name', 'update bottle')->first();
$this->user->givePermissionTo($updatePermission);
$bottle = Bottle::factory()->create();
$bottle->outlets()->attach([$this->outlets->first()->id]);
$newOutlets = [$this->outlets[1]->id, $this->outlets[2]->id];
mountBottleEditComponent($this->user, $bottle)
->set('form.name', 'Updated Integration Bottle')
->set('form.size', '300')
->set('form.sale_price', '90000')
->set('form.outlet_ids', $newOutlets)
->call('save')
->assertHasNoErrors();
$bottle->refresh();
expect($bottle->name)->toBe('Updated Integration Bottle');
expect($bottle->size)->toBe(300);
expect($bottle->outlets)->toHaveCount(2);
expect($bottle->outlets->pluck('id')->toArray())->toBe($newOutlets);
});
/*
|--------------------------------------------------------------------------
| Error Handling and Edge Cases
|--------------------------------------------------------------------------
*/
it('handles invalid bottle hash in edit component', function () {
expect(fn () => mountBottleEditComponent($this->user, 'invalid-hash'))
->toThrow(\TypeError::class);
});
it('validates bottle form data integrity during operations', function () {
// Test that form data is properly reset between operations
$bottle1 = Bottle::factory()->create(['name' => 'First Bottle']);
$bottle2 = Bottle::factory()->create(['name' => 'Second Bottle']);
$component = mountBottleEditComponent($this->user, $bottle1);
// Open modal for first bottle
expect($component->form->name)->toBe('First Bottle');
// Verify first bottle wasn't modified
$bottle1->refresh();
expect($bottle1->name)->toBe('First Bottle');
});

View File

@ -1,115 +0,0 @@
<?php
use App\Models\Bottle;
use App\Models\Employee;
use App\Models\User;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::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 mountBottleIndexComponent(User $user)
{
return Livewire::actingAs($user)->test(\App\Livewire\Studio\Catalog\Bottle\Index::class);
}
/*
|--------------------------------------------------------------------------
| Index Component Rendering & Basic Functionality
|--------------------------------------------------------------------------
*/
it('renders bottle index page successfully', function () {
mountBottleIndexComponent($this->user)
->assertViewIs('livewire.studio.catalog.bottle.index')
->assertViewHas('pageTitle', 'Botol');
});
it('displays bottles correctly', function () {
Bottle::factory()->count(3)->create();
// Just check that the component renders without errors and has bottles
$component = mountBottleIndexComponent($this->user);
$component->assertViewIs('livewire.studio.catalog.bottle.index');
expect(Bottle::count())->toBe(3);
});
/*
|--------------------------------------------------------------------------
| Delete Bottle Tests
|--------------------------------------------------------------------------
*/
it('deletes bottle successfully when authorized', function () {
$bottle = Bottle::factory()->create([
'name' => 'Bottle to Delete',
]);
mountBottleIndexComponent($this->user)
->call('delete', $bottle)
->assertHasNoErrors()
->assertDispatched('refreshDatatable');
$this->assertSoftDeleted('bottles', [
'id' => $bottle->id,
]);
});
it('prevents bottle deletion when unauthorized', function () {
$this->ownerRole->revokePermissionTo('delete bottle');
$bottle = Bottle::factory()->create();
mountBottleIndexComponent($this->user)
->call('delete', $bottle)
->assertForbidden();
});
/*
|--------------------------------------------------------------------------
| Authorization Edge Cases
|--------------------------------------------------------------------------
*/
it('handles unauthorized user with revoked permissions', function () {
// Revoke all bottle permissions
$this->ownerRole->revokePermissionTo(['delete bottle']);
$bottle = Bottle::factory()->create();
// Test delete
mountBottleIndexComponent($this->user)
->call('delete', $bottle)
->assertForbidden();
});
it('handles user with partial permissions correctly', function () {
// Remove owner role and create a user with limited permissions
$this->user->roles()->detach();
$limitedRole = Role::create(['name' => 'Limited User']);
$this->user->roles()->attach($limitedRole->id);
// Give only some permissions but not delete
$limitedRole->givePermissionTo([]);
$bottle = Bottle::factory()->create();
// Should not be able to delete
mountBottleIndexComponent($this->user)
->call('delete', $bottle)
->assertForbidden();
});

View File

@ -1,573 +0,0 @@
<?php
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);
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();
mountBrandComponent($this->user)
->assertSee($brand->name);
});
/*
|--------------------------------------------------------------------------
| Open Modal Tests
|--------------------------------------------------------------------------
*/
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'])
->call('create')
->assertHasNoErrors()
->assertDispatched('refreshDatatable');
expect(BrandModel::count())->toBe(1);
$brand = BrandModel::first();
expect($brand->name)->toBe($data['name']);
expect($brand->sort_order)->toBe(1);
});
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();
Gate::define('update brand', fn () => false);
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('Updated Name');
});
it('updates brand without changing sort_order', function () {
$brand = BrandModel::factory()->create([
'name' => 'Original Name',
'sort_order' => 5,
]);
$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);
});

View File

@ -1,925 +0,0 @@
<?php
use App\Livewire\Studio\Catalog\Category;
use App\Models\Category as CategoryModel;
use App\Models\Employee;
use App\Models\PushNotification;
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);
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();
mountCategoryComponent($this->user)
->assertSee($category->name);
});
/*
|--------------------------------------------------------------------------
| Open Modal Tests
|--------------------------------------------------------------------------
*/
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.description', $data['description'])
->call('create')
->assertHasNoErrors()
->assertDispatched('refreshDatatable');
expect(CategoryModel::count())->toBe(1);
$category = CategoryModel::first();
expect($category->name)->toBe($data['name']);
expect($category->description)->toBe($data['description']);
expect($category->sort_order)->toBe(1);
});
it('creates category with sort_order as first position', 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(1);
});
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();
Gate::define('update category', fn () => false);
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('Updated Name');
expect($category->description)->toBe('Updated Description');
});
it('updates category without changing sort_order', function () {
$permission = Permission::where('name', 'update category')->first();
$this->user->givePermissionTo($permission);
$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);
});
/*
|--------------------------------------------------------------------------
| Notification Subscription Tests
|--------------------------------------------------------------------------
*/
it('subscribes to push notifications successfully', function () {
$subscription = [
'endpoint' => 'https://fcm.googleapis.com/fcm/send/example',
'keys' => [
'p256dh' => 'example_p256dh_key',
'auth' => 'example_auth_key',
],
];
mountCategoryComponent($this->user)
->call('notificationSubscribe', json_encode($subscription));
expect(PushNotification::count())->toBe(1);
$notification = PushNotification::first();
expect($notification->user_id)->toBe($this->user->id);
expect($notification->subscriptions)->toBe(json_encode($subscription));
});
it('does not create duplicate push notification subscription', function () {
$subscription = [
'endpoint' => 'https://fcm.googleapis.com/fcm/send/example',
'keys' => [
'p256dh' => 'example_p256dh_key',
'auth' => 'example_auth_key',
],
];
// Subscribe first time
mountCategoryComponent($this->user)
->call('notificationSubscribe', json_encode($subscription));
// Try to subscribe again with same auth key
mountCategoryComponent($this->user)
->call('notificationSubscribe', json_encode($subscription));
expect(PushNotification::count())->toBe(1);
});
it('creates new push notification subscription for different auth keys', function () {
// Create existing subscription
PushNotification::create([
'user_id' => $this->user->id,
'subscriptions' => json_encode([
'endpoint' => 'old_endpoint',
'keys' => ['p256dh' => 'old_p256dh', 'auth' => 'old_auth'],
]),
]);
$newSubscription = [
'endpoint' => 'https://fcm.googleapis.com/fcm/send/new_example',
'keys' => [
'p256dh' => 'new_p256dh_key',
'auth' => 'new_auth_key', // Different auth key
],
];
mountCategoryComponent($this->user)
->call('notificationSubscribe', json_encode($newSubscription));
// Should create a new record since auth keys are different
expect(PushNotification::count())->toBe(2);
$notifications = PushNotification::where('user_id', $this->user->id)->get();
expect($notifications)->toHaveCount(2);
});
/*
|--------------------------------------------------------------------------
| Confirmation Dialog Tests
|--------------------------------------------------------------------------
*/
it('handles confirmation action for delete', function () {
$category = CategoryModel::factory()->create();
mountCategoryComponent($this->user)
->call('confirmAction', $category->hash, 'delete');
expect($this->user->fresh()->confirmingId)->toBeNull(); // Property is on component, not user
});
it('sets confirming properties correctly', function () {
$component = mountCategoryComponent($this->user);
$component->call('confirmAction', 'test-id', 'custom_action');
// Since these are public properties on the component, we can access them
// But Livewire components don't expose these properties directly in tests
// Let's test the delete method which should work with confirmation
$category = CategoryModel::factory()->create();
mountCategoryComponent($this->user)
->call('delete', $category)
->assertHasNoErrors();
});
/*
|--------------------------------------------------------------------------
| Toast Notification Tests
|--------------------------------------------------------------------------
*/
it('shows success toast on category creation', function () {
mountCategoryComponent($this->user)
->call('openModal', 'create', 'Tambah Kategori')
->set('form.name', 'Test Category')
->call('create')
->assertHasNoErrors();
// Toast assertions are implicit in the component's toast() calls
// We can verify the category was created successfully
expect(CategoryModel::where('name', 'Test Category')->exists())->toBeTrue();
});
it('shows success toast on category update', function () {
$permission = Permission::where('name', 'update category')->first();
$this->user->givePermissionTo($permission);
$category = CategoryModel::factory()->create(['name' => 'Old Name']);
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');
});
it('shows success toast on category deletion', function () {
$category = CategoryModel::factory()->create(['name' => 'Category to Delete']);
mountCategoryComponent($this->user)
->call('delete', $category)
->assertHasNoErrors();
$this->assertSoftDeleted('categories', ['id' => $category->id]);
});
/*
|--------------------------------------------------------------------------
| Real-time Validation Tests
|--------------------------------------------------------------------------
*/
it('validates name field in real-time on update', function () {
$component = mountCategoryComponent($this->user)
->call('openModal', 'create', 'Tambah Kategori');
// Set empty name - should trigger validation
$component->set('form.name', '')
->assertHasErrors(['form.name']);
// Set valid name - should clear errors
$component->set('form.name', 'Valid Category Name')
->assertHasNoErrors(['form.name']);
});
it('validates name length in real-time', function () {
$component = mountCategoryComponent($this->user)
->call('openModal', 'create', 'Tambah Kategori');
// Set name longer than 20 characters
$component->set('form.name', str_repeat('a', 21))
->assertHasErrors(['form.name']);
// Set valid length name
$component->set('form.name', str_repeat('a', 20))
->assertHasNoErrors(['form.name']);
});
it('validates name uniqueness in real-time', function () {
$existingCategory = CategoryModel::factory()->create(['name' => 'Existing Name']);
$component = mountCategoryComponent($this->user)
->call('openModal', 'create', 'Tambah Kategori');
// Try to set duplicate name
$component->set('form.name', 'Existing Name')
->assertHasErrors(['form.name']);
// Set unique name
$component->set('form.name', 'Unique Name')
->assertHasNoErrors(['form.name']);
});
/*
|--------------------------------------------------------------------------
| Authorization Edge Cases Tests
|--------------------------------------------------------------------------
*/
it('handles unauthorized user with revoked permissions', function () {
// Revoke all category permissions
$this->ownerRole->revokePermissionTo(['create category', 'update category', 'delete category']);
// Define gates to return false
Gate::define('create category', fn () => false);
Gate::define('update category', fn () => false);
Gate::define('delete category', fn () => false);
$category = CategoryModel::factory()->create();
// Test create
mountCategoryComponent($this->user)
->call('create')
->assertForbidden();
// Test update
mountCategoryComponent($this->user)
->call('update')
->assertForbidden();
// Test delete
mountCategoryComponent($this->user)
->call('delete', $category)
->assertForbidden();
// Test sort order
mountCategoryComponent($this->user)
->call('sortOrder', $category, 'up')
->assertForbidden();
});
it('handles user with partial permissions correctly', function () {
// Remove owner role and create a user with limited permissions
$this->user->roles()->detach();
$limitedRole = Role::create(['name' => 'Limited User']);
$this->user->roles()->attach($limitedRole->id);
// Give only create permission
$createPermission = Permission::where('name', 'create category')->first();
$limitedRole->givePermissionTo($createPermission);
$category = CategoryModel::factory()->create();
// Should be able to create
mountCategoryComponent($this->user)
->call('openModal', 'create', 'Tambah Kategori')
->set('form.name', 'New Category')
->call('create')
->assertHasNoErrors();
// Should not be able to update
mountCategoryComponent($this->user)
->call('openModal', 'update', 'Edit Kategori', $category->hash)
->set('form.name', 'Updated Name')
->call('update')
->assertForbidden();
// Should not be able to delete
mountCategoryComponent($this->user)
->call('delete', $category)
->assertForbidden();
// Should not be able to sort
mountCategoryComponent($this->user)
->call('sortOrder', $category, 'up')
->assertForbidden();
});
/*
|--------------------------------------------------------------------------
| Error Handling and Edge Cases Tests
|--------------------------------------------------------------------------
*/
it('handles invalid category hash by throwing exception', function () {
// The byHashOrFail method throws ModelNotFoundException for invalid hashes
// This is expected behavior - invalid hashes should result in errors
expect(fn () => mountCategoryComponent($this->user)
->call('openModal', 'update', 'Edit Kategori', 'invalid-hash'))
->toThrow(\Illuminate\Database\Eloquent\ModelNotFoundException::class);
});
it('handles sort order operations on empty database', function () {
// Ensure no categories exist
CategoryModel::query()->delete();
$category = CategoryModel::factory()->create();
// Test moving up when it's the only/first category
mountCategoryComponent($this->user)
->call('sortOrder', $category, 'up')
->assertHasNoErrors();
// Test moving down when it's the only/last category
mountCategoryComponent($this->user)
->call('sortOrder', $category, 'down')
->assertHasNoErrors();
// Test moving to first when it's already first
mountCategoryComponent($this->user)
->call('sortOrder', $category, 'first')
->assertHasNoErrors();
// Test moving to last when it's already last
mountCategoryComponent($this->user)
->call('sortOrder', $category, 'last')
->assertHasNoErrors();
});
it('handles concurrent category creation correctly', function () {
// Create multiple categories in sequence to test sort_order as first position
for ($i = 1; $i <= 5; $i++) {
mountCategoryComponent($this->user)
->call('openModal', 'create', 'Tambah Kategori')
->set('form.name', "Category {$i}")
->call('create')
->assertHasNoErrors();
$category = CategoryModel::where('name', "Category {$i}")->first();
expect($category->sort_order)->toBe(1); // New categories always get sort_order = 1 (first position)
}
expect(CategoryModel::count())->toBe(5);
});
it('handles category deletion with cascade relationships', function () {
// Note: Category model has cascadeDeletes = ['perfumes']
// This test ensures the deletion works even with related models
$category = CategoryModel::factory()->create();
mountCategoryComponent($this->user)
->call('delete', $category)
->assertHasNoErrors();
$this->assertSoftDeleted('categories', ['id' => $category->id]);
});
it('validates form data integrity during operations', function () {
// Test that form data is properly reset between operations
$category1 = CategoryModel::factory()->create(['name' => 'First Category']);
$category2 = CategoryModel::factory()->create(['name' => 'Second Category']);
$component = mountCategoryComponent($this->user);
// Open modal for first category
$component->call('openModal', 'update', 'Edit Kategori', $category1->hash);
expect($component->form->name)->toBe('First Category');
// Open modal for second category without saving
$component->call('openModal', 'update', 'Edit Kategori', $category2->hash);
expect($component->form->name)->toBe('Second Category');
// Verify first category wasn't modified
$category1->refresh();
expect($category1->name)->toBe('First Category');
});

View File

@ -1,475 +0,0 @@
<?php
use App\Enums\AuditStockStatus;
use App\Models\Employee;
use App\Models\Outlet;
use App\Models\Perfume;
use App\Models\User;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Spatie\Activitylog\Models\Activity;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::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);
// Create test outlets
$this->outlets = Outlet::factory()->count(3)->create();
});
function mountPerfumeAuditComponent(User $user, Outlet $outlet, Perfume $perfume)
{
return Livewire::actingAs($user)->test(\App\Livewire\Studio\Catalog\Perfume\Audit::class, ['outlet' => $outlet, 'perfume' => $perfume]);
}
/*
|--------------------------------------------------------------------------
| Audit Component Rendering & Mount
|--------------------------------------------------------------------------
*/
it('renders perfume audit page successfully', function () {
$perfume = Perfume::factory()->create();
$outlet = $this->outlets->first();
mountPerfumeAuditComponent($this->user, $outlet, $perfume)
->assertViewIs('components.sections.ui.audit')
->assertViewHas('pageTitle', "Audit {$perfume->name} di {$outlet->name}")
->assertViewHas('module', 'perfume');
});
it('sets perfume label correctly on mount', function () {
$perfume = Perfume::factory()->create(['name' => 'Test Perfume']);
$outlet = $this->outlets->first();
$component = mountPerfumeAuditComponent($this->user, $outlet, $perfume);
expect($component->label)->toBe('Test Perfume di '.$outlet->name);
});
/*
|--------------------------------------------------------------------------
| Activity Logs Display Tests
|--------------------------------------------------------------------------
*/
it('displays perfume activity logs correctly', function () {
$perfume = Perfume::factory()->create();
$outlet = $this->outlets->first();
// Create some activity logs with expected properties
Activity::create([
'log_name' => 'default',
'description' => 'Stock updated',
'subject_type' => get_class($perfume),
'subject_id' => $perfume->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 5,
'quantity_change' => 10,
'new_stock' => 15,
],
'event' => AuditStockStatus::INCREASED->value,
]);
Activity::create([
'log_name' => 'default',
'description' => 'Stock decreased',
'subject_type' => get_class($perfume),
'subject_id' => $perfume->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 15,
'quantity_change' => -5,
'new_stock' => 10,
],
'event' => AuditStockStatus::DECREASED->value,
]);
$component = mountPerfumeAuditComponent($this->user, $outlet, $perfume);
expect($component->activityLogs)->toHaveCount(2);
expect($component->activityLogs->first()->event['label'])->toBe(AuditStockStatus::INCREASED->label());
expect($component->activityLogs->first()->event['color'])->toBe(AuditStockStatus::INCREASED->color());
expect($component->activityLogs->last()->event['label'])->toBe(AuditStockStatus::DECREASED->label());
expect($component->activityLogs->last()->event['color'])->toBe(AuditStockStatus::DECREASED->color());
});
it('filters perfume activity logs by outlet', function () {
$perfume = Perfume::factory()->create();
$outlet1 = $this->outlets->first();
$outlet2 = $this->outlets->last();
// Create activity for outlet1
Activity::create([
'log_name' => 'default',
'description' => 'Stock updated outlet1',
'subject_type' => get_class($perfume),
'subject_id' => $perfume->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet1->id,
'previous_stock' => 5,
'quantity_change' => 10,
'new_stock' => 15,
],
'event' => AuditStockStatus::UPDATED->value,
]);
// Create activity for outlet2
Activity::create([
'log_name' => 'default',
'description' => 'Stock increased outlet2',
'subject_type' => get_class($perfume),
'subject_id' => $perfume->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet2->id,
'previous_stock' => 0,
'quantity_change' => 20,
'new_stock' => 20,
],
'event' => AuditStockStatus::INCREASED->value,
]);
$component1 = mountPerfumeAuditComponent($this->user, $outlet1, $perfume);
$component2 = mountPerfumeAuditComponent($this->user, $outlet2, $perfume);
expect($component1->activityLogs)->toHaveCount(1);
expect($component1->activityLogs[0]['description'])->toBe('Stock updated outlet1');
expect($component2->activityLogs)->toHaveCount(1);
expect($component2->activityLogs[0]['description'])->toBe('Stock increased outlet2');
});
it('orders perfume activity logs by latest first', function () {
$perfume = Perfume::factory()->create();
$outlet = $this->outlets->first();
// Create activities with different timestamps
$activity1 = Activity::create([
'log_name' => 'default',
'description' => 'first activity',
'subject_type' => get_class($perfume),
'subject_id' => $perfume->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 5,
'quantity_change' => 5,
'new_stock' => 10,
],
'event' => AuditStockStatus::UPDATED->value,
'created_at' => now()->subMinutes(5),
]);
$activity2 = Activity::create([
'log_name' => 'default',
'description' => 'second activity',
'subject_type' => get_class($perfume),
'subject_id' => $perfume->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 10,
'quantity_change' => 10,
'new_stock' => 20,
],
'event' => AuditStockStatus::INCREASED->value,
'created_at' => now(),
]);
$component = mountPerfumeAuditComponent($this->user, $outlet, $perfume);
expect($component->activityLogs)->toHaveCount(2);
expect($component->activityLogs[0]['description'])->toBe('second activity');
expect($component->activityLogs[1]['description'])->toBe('first activity');
});
/*
|--------------------------------------------------------------------------
| Activity Logs Content Tests
|--------------------------------------------------------------------------
*/
it('maps perfume activity events to correct labels and colors', function () {
$perfume = Perfume::factory()->create();
$outlet = $this->outlets->first();
// Test INCREASED event
Activity::create([
'log_name' => 'default',
'description' => 'Stock increased',
'subject_type' => get_class($perfume),
'subject_id' => $perfume->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 10,
'quantity_change' => 5,
'new_stock' => 15,
],
'event' => AuditStockStatus::INCREASED->value,
]);
// Test DECREASED event
Activity::create([
'log_name' => 'default',
'description' => 'Stock decreased',
'subject_type' => get_class($perfume),
'subject_id' => $perfume->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 15,
'quantity_change' => -3,
'new_stock' => 12,
],
'event' => AuditStockStatus::DECREASED->value,
]);
// Test UPDATED event
Activity::create([
'log_name' => 'default',
'description' => 'Stock updated',
'subject_type' => get_class($perfume),
'subject_id' => $perfume->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 12,
'quantity_change' => 0,
'new_stock' => 12,
],
'event' => AuditStockStatus::UPDATED->value,
]);
$component = mountPerfumeAuditComponent($this->user, $outlet, $perfume);
expect($component->activityLogs)->toHaveCount(3);
// Check INCREASED event mapping
$increasedLog = collect($component->activityLogs)->firstWhere('event.label', AuditStockStatus::INCREASED->label());
expect($increasedLog['event']['color'])->toBe(AuditStockStatus::INCREASED->color());
// Check DECREASED event mapping
$decreasedLog = collect($component->activityLogs)->firstWhere('event.label', AuditStockStatus::DECREASED->label());
expect($decreasedLog['event']['color'])->toBe(AuditStockStatus::DECREASED->color());
// Check UPDATED event mapping
$updatedLog = collect($component->activityLogs)->firstWhere('event.label', AuditStockStatus::UPDATED->label());
expect($updatedLog['event']['color'])->toBe(AuditStockStatus::UPDATED->color());
});
/*
|--------------------------------------------------------------------------
| Empty State Tests
|--------------------------------------------------------------------------
*/
it('handles empty perfume activity logs in audit component', function () {
$perfume = Perfume::factory()->create();
$outlet = $this->outlets->first();
$component = mountPerfumeAuditComponent($this->user, $outlet, $perfume);
expect($component->activityLogs)->toBeInstanceOf(\Illuminate\Database\Eloquent\Collection::class);
expect($component->activityLogs)->toHaveCount(0);
});
it('handles perfume with no activities in specific outlet', function () {
$perfume = Perfume::factory()->create();
$outlet1 = $this->outlets->first();
$outlet2 = $this->outlets->last();
// Create activity only for outlet2
Activity::create([
'log_name' => 'default',
'description' => 'Stock updated',
'subject_type' => get_class($perfume),
'subject_id' => $perfume->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet2->id,
'previous_stock' => 5,
'quantity_change' => 5,
'new_stock' => 10,
],
'event' => AuditStockStatus::UPDATED->value,
]);
$component1 = mountPerfumeAuditComponent($this->user, $outlet1, $perfume);
$component2 = mountPerfumeAuditComponent($this->user, $outlet2, $perfume);
expect($component1->activityLogs)->toHaveCount(0);
expect($component2->activityLogs)->toHaveCount(1);
});
/*
|--------------------------------------------------------------------------
| Integration Tests
|--------------------------------------------------------------------------
*/
it('displays real perfume activity log data correctly', function () {
$perfume = Perfume::factory()->create(['name' => 'Integration Perfume']);
$outlet = $this->outlets->first();
$activity = Activity::create([
'log_name' => 'default',
'description' => 'Stock adjustment for integration test',
'subject_type' => get_class($perfume),
'subject_id' => $perfume->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 100,
'quantity_change' => 25,
'new_stock' => 125,
],
'event' => AuditStockStatus::INCREASED->value,
]);
$component = mountPerfumeAuditComponent($this->user, $outlet, $perfume);
expect($component->activityLogs)->toHaveCount(1);
$log = $component->activityLogs->first();
expect($log->description)->toBe('Stock adjustment for integration test');
expect($log->properties['previous_stock'])->toBe(100);
expect($log->properties['quantity_change'])->toBe(25);
expect($log->properties['new_stock'])->toBe(125);
expect($log->event['label'])->toBe(AuditStockStatus::INCREASED->label());
expect($log->event['color'])->toBe(AuditStockStatus::INCREASED->color());
});
it('handles multiple perfume activities across different outlets', function () {
$perfume = Perfume::factory()->create();
$outlet1 = $this->outlets->first();
$outlet2 = $this->outlets->last();
// Create activities for both outlets
Activity::create([
'log_name' => 'default',
'description' => 'Activity in outlet 1',
'subject_type' => get_class($perfume),
'subject_id' => $perfume->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet1->id,
'previous_stock' => 10,
'quantity_change' => 5,
'new_stock' => 15,
],
'event' => AuditStockStatus::INCREASED->value,
]);
Activity::create([
'log_name' => 'default',
'description' => 'Activity in outlet 2',
'subject_type' => get_class($perfume),
'subject_id' => $perfume->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet2->id,
'previous_stock' => 20,
'quantity_change' => -3,
'new_stock' => 17,
],
'event' => AuditStockStatus::DECREASED->value,
]);
$component1 = mountPerfumeAuditComponent($this->user, $outlet1, $perfume);
$component2 = mountPerfumeAuditComponent($this->user, $outlet2, $perfume);
expect($component1->activityLogs)->toHaveCount(1);
expect($component1->activityLogs[0]['description'])->toBe('Activity in outlet 1');
expect($component2->activityLogs)->toHaveCount(1);
expect($component2->activityLogs[0]['description'])->toBe('Activity in outlet 2');
});
/*
|--------------------------------------------------------------------------
| Edge Cases and Error Handling
|--------------------------------------------------------------------------
*/
it('handles perfume activity logs with missing properties gracefully', function () {
$perfume = Perfume::factory()->create();
$outlet = $this->outlets->first();
// Create activity with incomplete properties (shouldn't happen in real usage)
Activity::create([
'log_name' => 'default',
'description' => 'Incomplete activity',
'subject_type' => get_class($perfume),
'subject_id' => $perfume->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 0, // Add required properties
'quantity_change' => 0,
'new_stock' => 0,
],
'event' => AuditStockStatus::UPDATED->value,
]);
$component = mountPerfumeAuditComponent($this->user, $outlet, $perfume);
expect($component->activityLogs)->toHaveCount(1);
// Component should render correctly with all required properties
});
it('handles perfume activities with null causer', function () {
$perfume = Perfume::factory()->create();
$outlet = $this->outlets->first();
Activity::create([
'log_name' => 'default',
'description' => 'Activity with no causer',
'subject_type' => get_class($perfume),
'subject_id' => $perfume->id,
'causer_type' => null,
'causer_id' => null,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 5,
'quantity_change' => 5,
'new_stock' => 10,
],
'event' => AuditStockStatus::INCREASED->value,
]);
$component = mountPerfumeAuditComponent($this->user, $outlet, $perfume);
expect($component->activityLogs)->toHaveCount(1);
expect($component->activityLogs[0]['user'])->toBeNull();
});

View File

@ -1,455 +0,0 @@
<?php
use App\Enums\Concentration;
use App\Models\Brand;
use App\Models\Category;
use App\Models\Employee;
use App\Models\Outlet;
use App\Models\Perfume;
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);
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);
// Create test data
$this->outlets = Outlet::factory()->count(3)->create();
$this->brands = Brand::factory()->count(2)->create();
$this->categories = Category::factory()->count(3)->create();
});
function mountPerfumeCreateComponent(User $user)
{
return Livewire::actingAs($user)->test(\App\Livewire\Studio\Catalog\Perfume\Create::class);
}
/*
|--------------------------------------------------------------------------
| Create Component Rendering & Mount
|--------------------------------------------------------------------------
*/
it('renders perfume create page successfully', function () {
mountPerfumeCreateComponent($this->user)
->assertViewIs('livewire.studio.catalog.perfume.form')
->assertViewHas('pageTitle', 'Tambah Parfum');
});
it('initializes perfume form properties on mount', function () {
$component = mountPerfumeCreateComponent($this->user);
expect($component->form->name)->toBe('');
expect($component->form->sku)->toBe('');
expect($component->form->brand_id)->toBeNull();
expect($component->form->cost_price)->toBe('');
expect($component->form->sale_price)->toBe('');
expect($component->form->concentration)->toBe('2');
expect($component->form->base_notes)->toBeNull();
expect($component->form->middle_notes)->toBeNull();
expect($component->form->top_notes)->toBeNull();
expect($component->form->description)->toBeNull();
expect($component->form->category_ids)->toBe([]);
expect($component->form->outlet_ids)->toBe([]);
expect($component->form->image)->toBe([]);
expect($component->outlets)->toHaveCount(3);
expect($component->brands)->toHaveCount(2);
expect($component->categories)->toHaveCount(3);
});
/*
|--------------------------------------------------------------------------
| Create Perfume Tests
|--------------------------------------------------------------------------
*/
it('creates perfume successfully when authorized', function () {
$data = [
'name' => 'New Perfume',
'sku' => 'NP001',
'brand_id' => $this->brands->first()->id,
'cost_price' => '150000',
'sale_price' => '250000',
'concentration' => Concentration::EAU_DE_PERFUME->value,
'base_notes' => 'Vanilla, Musk',
'middle_notes' => 'Jasmine, Rose',
'top_notes' => 'Bergamot, Lemon',
'description' => 'A beautiful fragrance',
'category_ids' => [$this->categories->first()->id, $this->categories->last()->id],
'outlet_ids' => [$this->outlets->first()->id, $this->outlets->last()->id],
];
mountPerfumeCreateComponent($this->user)
->set('form.name', $data['name'])
->set('form.sku', $data['sku'])
->set('form.brand_id', $data['brand_id'])
->set('form.cost_price', $data['cost_price'])
->set('form.sale_price', $data['sale_price'])
->set('form.concentration', $data['concentration'])
->set('form.base_notes', $data['base_notes'])
->set('form.middle_notes', $data['middle_notes'])
->set('form.top_notes', $data['top_notes'])
->set('form.description', $data['description'])
->set('form.category_ids', $data['category_ids'])
->set('form.outlet_ids', $data['outlet_ids'])
->call('save')
->assertHasNoErrors()
->assertRedirect(route('studio.catalog.perfume.index'));
expect(Perfume::count())->toBe(1);
$perfume = Perfume::first();
expect($perfume->name)->toBe($data['name']);
expect($perfume->sku)->toBe($data['sku']);
expect($perfume->brand_id)->toBe($data['brand_id']);
expect($perfume->cost_price)->toBe(150000);
expect($perfume->sale_price)->toBe(250000);
expect($perfume->concentration->value)->toBe($data['concentration']);
expect($perfume->base_notes)->toBe($data['base_notes']);
expect($perfume->middle_notes)->toBe($data['middle_notes']);
expect($perfume->top_notes)->toBe($data['top_notes']);
expect($perfume->description)->toBe($data['description']);
expect($perfume->categories)->toHaveCount(2);
expect($perfume->outlets)->toHaveCount(2);
});
it('prevents perfume creation when unauthorized', function () {
$this->ownerRole->revokePermissionTo('create perfume');
Gate::define('create perfume', fn () => false);
mountPerfumeCreateComponent($this->user)
->set('form.name', 'Test Perfume')
->set('form.sku', 'TP001')
->set('form.sale_price', '100000')
->set('form.category_ids', [$this->categories->first()->id])
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertForbidden();
});
it('creates perfume with empty cost_price for non-owner roles', function () {
// Create user with limited role
$this->user->roles()->detach();
$limitedRole = Role::create(['name' => 'Cashier']);
$this->user->roles()->attach($limitedRole->id);
$createPermission = Permission::where('name', 'create perfume')->first();
$limitedRole->givePermissionTo($createPermission);
mountPerfumeCreateComponent($this->user)
->set('form.name', 'Test Perfume')
->set('form.sku', 'TP001')
->set('form.cost_price', '') // Empty cost price
->set('form.sale_price', '200000')
->set('form.category_ids', [$this->categories->first()->id])
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasNoErrors();
$perfume = Perfume::first();
expect($perfume->cost_price)->toBe(0);
});
it('creates perfume with optional brand', function () {
mountPerfumeCreateComponent($this->user)
->set('form.name', 'Perfume Without Brand')
->set('form.sku', 'PWB001')
->set('form.brand_id', null) // No brand
->set('form.cost_price', '100000') // Required for Owner role
->set('form.sale_price', '150000')
->set('form.category_ids', [$this->categories->first()->id])
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasNoErrors();
$perfume = Perfume::first();
expect($perfume->brand_id)->toBeNull();
});
/*
|--------------------------------------------------------------------------
| Form Validation Tests
|--------------------------------------------------------------------------
*/
it('validates required name field on perfume create', function () {
mountPerfumeCreateComponent($this->user)
->set('form.name', '')
->set('form.sku', 'TP001')
->set('form.sale_price', '100000')
->set('form.category_ids', [$this->categories->first()->id])
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasErrors(['form.name']);
});
it('validates name field max length on perfume create', function () {
mountPerfumeCreateComponent($this->user)
->set('form.name', str_repeat('a', 51))
->set('form.sku', 'TP001')
->set('form.sale_price', '100000')
->set('form.category_ids', [$this->categories->first()->id])
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasErrors(['form.name']);
});
it('validates required sku field on perfume create', function () {
mountPerfumeCreateComponent($this->user)
->set('form.name', 'Test Perfume')
->set('form.sku', '')
->set('form.sale_price', '100000')
->set('form.category_ids', [$this->categories->first()->id])
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasErrors(['form.sku']);
});
it('validates sku field max length on perfume create', function () {
mountPerfumeCreateComponent($this->user)
->set('form.name', 'Test Perfume')
->set('form.sku', str_repeat('a', 21))
->set('form.sale_price', '100000')
->set('form.category_ids', [$this->categories->first()->id])
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasErrors(['form.sku']);
});
it('validates sku uniqueness on perfume create', function () {
Perfume::factory()->create(['sku' => 'EXISTING001']);
mountPerfumeCreateComponent($this->user)
->set('form.name', 'Test Perfume')
->set('form.sku', 'EXISTING001')
->set('form.sale_price', '100000')
->set('form.category_ids', [$this->categories->first()->id])
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasErrors(['form.sku']);
});
it('validates brand_id exists if provided', function () {
mountPerfumeCreateComponent($this->user)
->set('form.name', 'Test Perfume')
->set('form.sku', 'TP001')
->set('form.brand_id', 999) // Non-existent brand
->set('form.sale_price', '100000')
->set('form.category_ids', [$this->categories->first()->id])
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasErrors(['form.brand_id']);
});
it('validates required sale_price field on perfume create', function () {
mountPerfumeCreateComponent($this->user)
->set('form.name', 'Test Perfume')
->set('form.sku', 'TP001')
->set('form.sale_price', '')
->set('form.category_ids', [$this->categories->first()->id])
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasErrors(['form.sale_price']);
});
it('validates concentration is valid enum value', function () {
mountPerfumeCreateComponent($this->user)
->set('form.name', 'Test Perfume')
->set('form.sku', 'TP001')
->set('form.sale_price', '100000')
->set('form.concentration', 'invalid')
->set('form.category_ids', [$this->categories->first()->id])
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasErrors(['form.concentration']);
});
it('validates required category_ids field on perfume create', function () {
mountPerfumeCreateComponent($this->user)
->set('form.name', 'Test Perfume')
->set('form.sku', 'TP001')
->set('form.sale_price', '100000')
->set('form.category_ids', [])
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasErrors(['form.category_ids']);
});
it('validates category_ids contains valid category ids', function () {
mountPerfumeCreateComponent($this->user)
->set('form.name', 'Test Perfume')
->set('form.sku', 'TP001')
->set('form.sale_price', '100000')
->set('form.category_ids', [999])
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasErrors(['form.category_ids.*']);
});
it('validates required outlet_ids field on perfume create', function () {
mountPerfumeCreateComponent($this->user)
->set('form.name', 'Test Perfume')
->set('form.sku', 'TP001')
->set('form.sale_price', '100000')
->set('form.category_ids', [$this->categories->first()->id])
->set('form.outlet_ids', [])
->call('save')
->assertHasErrors(['form.outlet_ids']);
});
it('validates outlet_ids contains valid outlet ids on perfume create', function () {
mountPerfumeCreateComponent($this->user)
->set('form.name', 'Test Perfume')
->set('form.sku', 'TP001')
->set('form.sale_price', '100000')
->set('form.category_ids', [$this->categories->first()->id])
->set('form.outlet_ids', [999])
->call('save')
->assertHasErrors(['form.outlet_ids.*']);
});
it('validates notes fields max length', function () {
mountPerfumeCreateComponent($this->user)
->set('form.name', 'Test Perfume')
->set('form.sku', 'TP001')
->set('form.sale_price', '100000')
->set('form.base_notes', str_repeat('a', 256)) // Exceeds 255
->set('form.category_ids', [$this->categories->first()->id])
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasErrors(['form.base_notes']);
});
/*
|--------------------------------------------------------------------------
| Real-time Validation Tests
|--------------------------------------------------------------------------
*/
it('validates perfume name field in real-time', function () {
$component = mountPerfumeCreateComponent($this->user);
// Valid name
$component->set('form.name', 'Valid Perfume Name')
->assertHasNoErrors(['form.name']);
// Empty name
$component->set('form.name', '')
->assertHasErrors(['form.name']);
// Name too long
$component->set('form.name', str_repeat('a', 51))
->assertHasErrors(['form.name']);
});
it('validates perfume sku field in real-time', function () {
$component = mountPerfumeCreateComponent($this->user);
// Valid SKU
$component->set('form.sku', 'VALID001')
->assertHasNoErrors(['form.sku']);
// Empty SKU
$component->set('form.sku', '')
->assertHasErrors(['form.sku']);
// SKU too long
$component->set('form.sku', str_repeat('a', 21))
->assertHasErrors(['form.sku']);
// Duplicate SKU
Perfume::factory()->create(['sku' => 'EXISTING001']);
$component->set('form.sku', 'EXISTING001')
->assertHasErrors(['form.sku']);
});
it('validates perfume price fields in real-time', function () {
$component = mountPerfumeCreateComponent($this->user);
// Valid sale price
$component->set('form.sale_price', '200000')
->assertHasNoErrors(['form.sale_price']);
// Empty sale price
$component->set('form.sale_price', '')
->assertHasErrors(['form.sale_price']);
// Negative sale price
$component->set('form.sale_price', '-200000')
->assertHasErrors(['form.sale_price']);
});
it('validates perfume concentration field in real-time', function () {
$component = mountPerfumeCreateComponent($this->user);
// Valid concentration
$component->set('form.concentration', Concentration::EAU_DE_PERFUME->value)
->assertHasNoErrors(['form.concentration']);
// Invalid concentration
$component->set('form.concentration', 'invalid')
->assertHasErrors(['form.concentration']);
});
/*
|--------------------------------------------------------------------------
| Integration Tests
|--------------------------------------------------------------------------
*/
it('creates perfume and attaches to categories and outlets correctly', function () {
$category1 = $this->categories->first();
$category2 = $this->categories->last();
$outlet1 = $this->outlets->first();
$outlet2 = $this->outlets->last();
$brand = $this->brands->first();
mountPerfumeCreateComponent($this->user)
->set('form.name', 'Integration Test Perfume')
->set('form.sku', 'ITP001')
->set('form.brand_id', $brand->id)
->set('form.cost_price', '200000') // Required for Owner role
->set('form.sale_price', '350000')
->set('form.concentration', Concentration::EAU_DE_PERFUME->value)
->set('form.category_ids', [$category1->id, $category2->id])
->set('form.outlet_ids', [$outlet1->id, $outlet2->id])
->call('save')
->assertHasNoErrors();
$perfume = Perfume::where('sku', 'ITP001')->first();
expect($perfume)->not->toBeNull();
expect($perfume->brand->id)->toBe($brand->id);
expect($perfume->categories)->toHaveCount(2);
expect($perfume->outlets)->toHaveCount(2);
expect($perfume->categories->pluck('id')->toArray())->toContain($category1->id, $category2->id);
expect($perfume->outlets->pluck('id')->toArray())->toContain($outlet1->id, $outlet2->id);
});
it('handles large numbers in perfume price fields', function () {
mountPerfumeCreateComponent($this->user)
->set('form.name', 'Expensive Perfume')
->set('form.sku', 'EP001')
->set('form.cost_price', '10000000') // 10 million
->set('form.sale_price', '15000000') // 15 million
->set('form.category_ids', [$this->categories->first()->id])
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasNoErrors();
$perfume = Perfume::where('sku', 'EP001')->first();
expect($perfume->cost_price)->toBe(10000000);
expect($perfume->sale_price)->toBe(15000000);
});

View File

@ -1,480 +0,0 @@
<?php
use App\Enums\Concentration;
use App\Models\Brand;
use App\Models\Category;
use App\Models\Employee;
use App\Models\Outlet;
use App\Models\Perfume;
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);
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);
// Create test data
$this->outlets = Outlet::factory()->count(3)->create();
$this->brands = Brand::factory()->count(2)->create();
$this->categories = Category::factory()->count(3)->create();
});
function mountPerfumeEditComponent(User $user, Perfume $perfume)
{
return Livewire::actingAs($user)->test(\App\Livewire\Studio\Catalog\Perfume\Edit::class, ['perfume' => $perfume->hash]);
}
/*
|--------------------------------------------------------------------------
| Edit Component Rendering & Mount
|--------------------------------------------------------------------------
*/
it('renders perfume edit page successfully', function () {
$perfume = Perfume::factory()->create();
mountPerfumeEditComponent($this->user, $perfume)
->assertViewIs('livewire.studio.catalog.perfume.form')
->assertViewHas('pageTitle', 'Ubah Parfum');
});
it('populates perfume form with perfume data on mount', function () {
$brand = $this->brands->first();
$perfume = Perfume::factory()->create([
'name' => 'Test Perfume',
'sku' => 'TP001',
'brand_id' => $brand->id,
'cost_price' => 150000,
'sale_price' => 250000,
'concentration' => Concentration::EAU_DE_PERFUME,
'base_notes' => 'Vanilla, Musk',
'middle_notes' => 'Jasmine, Rose',
'top_notes' => 'Bergamot, Lemon',
'description' => 'Test description',
]);
$perfume->categories()->attach([$this->categories->first()->id]);
$perfume->outlets()->attach([$this->outlets->first()->id]);
$component = mountPerfumeEditComponent($this->user, $perfume);
expect($component->form->name)->toBe('Test Perfume');
expect($component->form->sku)->toBe('TP001');
expect($component->form->brand_id)->toBe((string) $brand->id);
expect($component->form->cost_price)->toBe('150000');
expect($component->form->sale_price)->toBe('250000');
expect($component->form->concentration)->toBe((string) Concentration::EAU_DE_PERFUME->value);
expect($component->form->base_notes)->toBe('Vanilla, Musk');
expect($component->form->middle_notes)->toBe('Jasmine, Rose');
expect($component->form->top_notes)->toBe('Bergamot, Lemon');
expect($component->form->description)->toBe('Test description');
expect($component->form->category_ids)->toBe([$this->categories->first()->id]);
expect($component->form->outlet_ids)->toBe([$this->outlets->first()->id]);
});
/*
|--------------------------------------------------------------------------
| Update Perfume Tests
|--------------------------------------------------------------------------
*/
it('updates perfume successfully when authorized', function () {
$updatePermission = Permission::where('name', 'update perfume')->first();
$this->user->givePermissionTo($updatePermission);
$oldBrand = $this->brands->first();
$perfume = Perfume::factory()->create([
'name' => 'Original Perfume',
'sku' => 'OP001',
'brand_id' => $oldBrand->id,
'cost_price' => 100000,
'sale_price' => 200000,
'concentration' => Concentration::EAU_DE_TOILETTE,
]);
$perfume->categories()->attach([$this->categories->first()->id]);
$perfume->outlets()->attach([$this->outlets->first()->id]);
$newBrand = $this->brands->last();
$updateData = [
'name' => 'Updated Perfume',
'sku' => 'UP001',
'brand_id' => $newBrand->id,
'cost_price' => '150000',
'sale_price' => '300000',
'concentration' => Concentration::EAU_DE_PERFUME->value,
'base_notes' => 'Updated Base Notes',
'middle_notes' => 'Updated Middle Notes',
'top_notes' => 'Updated Top Notes',
'description' => 'Updated description',
'category_ids' => [$this->categories->first()->id, $this->categories->last()->id],
'outlet_ids' => [$this->outlets->first()->id, $this->outlets->last()->id],
];
mountPerfumeEditComponent($this->user, $perfume)
->set('form.name', $updateData['name'])
->set('form.sku', $updateData['sku'])
->set('form.brand_id', $updateData['brand_id'])
->set('form.cost_price', $updateData['cost_price'])
->set('form.sale_price', $updateData['sale_price'])
->set('form.concentration', $updateData['concentration'])
->set('form.base_notes', $updateData['base_notes'])
->set('form.middle_notes', $updateData['middle_notes'])
->set('form.top_notes', $updateData['top_notes'])
->set('form.description', $updateData['description'])
->set('form.category_ids', $updateData['category_ids'])
->set('form.outlet_ids', $updateData['outlet_ids'])
->call('save')
->assertHasNoErrors()
->assertRedirect(route('studio.catalog.perfume.index'));
$perfume->refresh();
expect($perfume->name)->toBe($updateData['name']);
expect($perfume->sku)->toBe($updateData['sku']);
expect($perfume->brand_id)->toBe($updateData['brand_id']);
expect($perfume->cost_price)->toBe(150000);
expect($perfume->sale_price)->toBe(300000);
expect($perfume->concentration->value)->toBe($updateData['concentration']);
expect($perfume->base_notes)->toBe($updateData['base_notes']);
expect($perfume->middle_notes)->toBe($updateData['middle_notes']);
expect($perfume->top_notes)->toBe($updateData['top_notes']);
expect($perfume->description)->toBe($updateData['description']);
expect($perfume->categories)->toHaveCount(2);
expect($perfume->outlets)->toHaveCount(2);
});
it('prevents perfume update when unauthorized', function () {
$this->ownerRole->revokePermissionTo('update perfume');
Gate::define('update perfume', fn () => false);
$perfume = Perfume::factory()->create();
mountPerfumeEditComponent($this->user, $perfume)
->set('form.name', 'Updated Name')
->call('save')
->assertForbidden();
});
it('syncs perfume category and outlet relationships correctly on update', function () {
$updatePermission = Permission::where('name', 'update perfume')->first();
$this->user->givePermissionTo($updatePermission);
$perfume = Perfume::factory()->create();
$perfume->categories()->attach([$this->categories->first()->id]);
$perfume->outlets()->attach([$this->outlets->first()->id]);
// Change to different categories and outlets
mountPerfumeEditComponent($this->user, $perfume)
->set('form.name', 'Updated Perfume')
->set('form.sku', 'UP001')
->set('form.sale_price', '200000')
->set('form.category_ids', [$this->categories[1]->id, $this->categories[2]->id])
->set('form.outlet_ids', [$this->outlets[1]->id, $this->outlets[2]->id])
->call('save')
->assertHasNoErrors();
$perfume->refresh();
expect($perfume->categories)->toHaveCount(2);
expect($perfume->outlets)->toHaveCount(2);
expect($perfume->categories->pluck('id')->toArray())->toBe([$this->categories[1]->id, $this->categories[2]->id]);
expect($perfume->outlets->pluck('id')->toArray())->toBe([$this->outlets[1]->id, $this->outlets[2]->id]);
});
it('allows same sku on update of same perfume', function () {
$updatePermission = Permission::where('name', 'update perfume')->first();
$this->user->givePermissionTo($updatePermission);
$perfume = Perfume::factory()->create(['sku' => 'EXISTING001']);
mountPerfumeEditComponent($this->user, $perfume)
->set('form.name', 'Updated Perfume')
->set('form.sku', 'EXISTING001') // Same SKU as existing perfume
->set('form.sale_price', '200000')
->set('form.category_ids', [$this->categories->first()->id])
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasNoErrors();
});
it('updates perfume brand to null', function () {
$updatePermission = Permission::where('name', 'update perfume')->first();
$this->user->givePermissionTo($updatePermission);
$perfume = Perfume::factory()->create(['brand_id' => $this->brands->first()->id]);
mountPerfumeEditComponent($this->user, $perfume)
->set('form.name', 'Perfume Without Brand')
->set('form.sku', 'PWB001')
->set('form.brand_id', null) // Remove brand
->set('form.sale_price', '150000')
->set('form.category_ids', [$this->categories->first()->id])
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasNoErrors();
$perfume->refresh();
expect($perfume->brand_id)->toBeNull();
});
/*
|--------------------------------------------------------------------------
| Form Validation Tests on Edit
|--------------------------------------------------------------------------
*/
it('validates required name field on perfume update', function () {
$perfume = Perfume::factory()->create();
mountPerfumeEditComponent($this->user, $perfume)
->set('form.name', '')
->call('save')
->assertHasErrors(['form.name']);
});
it('validates name field max length on perfume update', function () {
$perfume = Perfume::factory()->create();
mountPerfumeEditComponent($this->user, $perfume)
->set('form.name', str_repeat('a', 51))
->call('save')
->assertHasErrors(['form.name']);
});
it('validates required sku field on perfume update', function () {
$perfume = Perfume::factory()->create();
mountPerfumeEditComponent($this->user, $perfume)
->set('form.sku', '')
->call('save')
->assertHasErrors(['form.sku']);
});
it('validates sku field max length on perfume update', function () {
$perfume = Perfume::factory()->create();
mountPerfumeEditComponent($this->user, $perfume)
->set('form.sku', str_repeat('a', 21))
->call('save')
->assertHasErrors(['form.sku']);
});
it('validates sku uniqueness on perfume update with different perfume', function () {
$perfume1 = Perfume::factory()->create(['sku' => 'SKU001']);
$perfume2 = Perfume::factory()->create(['sku' => 'SKU002']);
mountPerfumeEditComponent($this->user, $perfume1)
->set('form.sku', 'SKU002') // Try to use SKU from perfume2
->call('save')
->assertHasErrors(['form.sku']);
});
it('validates brand_id exists if provided on perfume update', function () {
$perfume = Perfume::factory()->create();
mountPerfumeEditComponent($this->user, $perfume)
->set('form.brand_id', 999) // Non-existent brand
->call('save')
->assertHasErrors(['form.brand_id']);
});
it('validates required sale_price field on perfume update', function () {
$perfume = Perfume::factory()->create();
mountPerfumeEditComponent($this->user, $perfume)
->set('form.sale_price', '')
->call('save')
->assertHasErrors(['form.sale_price']);
});
it('validates concentration is valid enum value on perfume update', function () {
$perfume = Perfume::factory()->create();
mountPerfumeEditComponent($this->user, $perfume)
->set('form.concentration', 'invalid')
->call('save')
->assertHasErrors(['form.concentration']);
});
it('validates required category_ids field on perfume update', function () {
$perfume = Perfume::factory()->create();
mountPerfumeEditComponent($this->user, $perfume)
->set('form.category_ids', [])
->call('save')
->assertHasErrors(['form.category_ids']);
});
it('validates category_ids contains valid category ids on perfume update', function () {
$perfume = Perfume::factory()->create();
mountPerfumeEditComponent($this->user, $perfume)
->set('form.category_ids', [999])
->call('save')
->assertHasErrors(['form.category_ids.*']);
});
it('validates required outlet_ids field on perfume update', function () {
$perfume = Perfume::factory()->create();
mountPerfumeEditComponent($this->user, $perfume)
->set('form.outlet_ids', [])
->call('save')
->assertHasErrors(['form.outlet_ids']);
});
it('validates outlet_ids contains valid outlet ids on perfume update', function () {
$perfume = Perfume::factory()->create();
mountPerfumeEditComponent($this->user, $perfume)
->set('form.outlet_ids', [999])
->call('save')
->assertHasErrors(['form.outlet_ids.*']);
});
/*
|--------------------------------------------------------------------------
| Real-time Validation Tests on Edit
|--------------------------------------------------------------------------
*/
it('validates perfume name field in real-time on edit', function () {
$perfume = Perfume::factory()->create();
$component = mountPerfumeEditComponent($this->user, $perfume);
// Valid name
$component->set('form.name', 'Valid Updated Perfume Name')
->assertHasNoErrors(['form.name']);
// Empty name
$component->set('form.name', '')
->assertHasErrors(['form.name']);
// Name too long
$component->set('form.name', str_repeat('a', 51))
->assertHasErrors(['form.name']);
});
it('validates perfume sku field in real-time on edit', function () {
$perfume = Perfume::factory()->create();
$component = mountPerfumeEditComponent($this->user, $perfume);
// Valid SKU
$component->set('form.sku', 'VALID001')
->assertHasNoErrors(['form.sku']);
// Empty SKU
$component->set('form.sku', '')
->assertHasErrors(['form.sku']);
// SKU too long
$component->set('form.sku', str_repeat('a', 21))
->assertHasErrors(['form.sku']);
});
it('validates perfume price fields in real-time on edit', function () {
$perfume = Perfume::factory()->create();
$component = mountPerfumeEditComponent($this->user, $perfume);
// Valid sale price
$component->set('form.sale_price', '300000')
->assertHasNoErrors(['form.sale_price']);
// Empty sale price
$component->set('form.sale_price', '')
->assertHasErrors(['form.sale_price']);
// Negative sale price
$component->set('form.sale_price', '-300000')
->assertHasErrors(['form.sale_price']);
});
it('validates perfume concentration field in real-time on edit', function () {
$perfume = Perfume::factory()->create();
$component = mountPerfumeEditComponent($this->user, $perfume);
// Valid concentration
$component->set('form.concentration', Concentration::EAU_DE_PERFUME->value)
->assertHasNoErrors(['form.concentration']);
// Invalid concentration
$component->set('form.concentration', 'invalid')
->assertHasErrors(['form.concentration']);
});
/*
|--------------------------------------------------------------------------
| Integration Tests
|--------------------------------------------------------------------------
*/
it('updates perfume and syncs category and outlet relationships', function () {
$updatePermission = Permission::where('name', 'update perfume')->first();
$this->user->givePermissionTo($updatePermission);
$perfume = Perfume::factory()->create();
$perfume->categories()->attach([$this->categories->first()->id]);
$perfume->outlets()->attach([$this->outlets->first()->id]);
$newCategories = [$this->categories[1]->id, $this->categories[2]->id];
$newOutlets = [$this->outlets[1]->id, $this->outlets[2]->id];
mountPerfumeEditComponent($this->user, $perfume)
->set('form.name', 'Updated Integration Perfume')
->set('form.sku', 'UIP001')
->set('form.sale_price', '400000')
->set('form.category_ids', $newCategories)
->set('form.outlet_ids', $newOutlets)
->call('save')
->assertHasNoErrors();
$perfume->refresh();
expect($perfume->name)->toBe('Updated Integration Perfume');
expect($perfume->categories)->toHaveCount(2);
expect($perfume->outlets)->toHaveCount(2);
expect($perfume->categories->pluck('id')->toArray())->toBe($newCategories);
expect($perfume->outlets->pluck('id')->toArray())->toBe($newOutlets);
});
/*
|--------------------------------------------------------------------------
| Error Handling and Edge Cases
|--------------------------------------------------------------------------
*/
it('handles invalid perfume hash in edit component', function () {
expect(fn () => mountPerfumeEditComponent($this->user, 'invalid-hash'))
->toThrow(\TypeError::class);
});
it('validates perfume form data integrity during operations', function () {
// Test that form data is properly reset between operations
$perfume1 = Perfume::factory()->create(['name' => 'First Perfume']);
$perfume2 = Perfume::factory()->create(['name' => 'Second Perfume']);
$component = mountPerfumeEditComponent($this->user, $perfume1);
// Open modal for first perfume
expect($component->form->name)->toBe('First Perfume');
// Verify first perfume wasn't modified
$perfume1->refresh();
expect($perfume1->name)->toBe('First Perfume');
});

View File

@ -1,118 +0,0 @@
<?php
use App\Models\Employee;
use App\Models\Perfume;
use App\Models\User;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::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 mountPerfumeIndexComponent(User $user)
{
return Livewire::actingAs($user)->test(\App\Livewire\Studio\Catalog\Perfume\Index::class);
}
/*
|--------------------------------------------------------------------------
| Index Component Rendering & Basic Functionality
|--------------------------------------------------------------------------
*/
it('renders perfume index page successfully', function () {
mountPerfumeIndexComponent($this->user)
->assertViewIs('livewire.studio.catalog.perfume.index')
->assertViewHas('pageTitle', 'Parfum');
});
it('displays perfumes correctly', function () {
Perfume::factory()->count(3)->create();
// Just check that the component renders without errors and has perfumes
$component = mountPerfumeIndexComponent($this->user);
$component->assertViewIs('livewire.studio.catalog.perfume.index');
expect(Perfume::count())->toBe(3);
});
/*
|--------------------------------------------------------------------------
| Delete Perfume Tests
|--------------------------------------------------------------------------
*/
it('deletes perfume successfully when authorized', function () {
$perfume = Perfume::factory()->create([
'name' => 'Perfume to Delete',
]);
mountPerfumeIndexComponent($this->user)
->call('delete', $perfume)
->assertHasNoErrors()
->assertDispatched('refreshDatatable');
$this->assertSoftDeleted('perfumes', [
'id' => $perfume->id,
]);
});
it('prevents perfume deletion when unauthorized', function () {
$this->ownerRole->revokePermissionTo('delete perfume');
$perfume = Perfume::factory()->create();
mountPerfumeIndexComponent($this->user)
->call('delete', $perfume)
->assertForbidden();
});
/*
|--------------------------------------------------------------------------
| Authorization Edge Cases
|--------------------------------------------------------------------------
*/
it('handles unauthorized user with revoked permissions', function () {
// Revoke all perfume permissions
$this->ownerRole->revokePermissionTo(['delete perfume']);
// Define gates to return false
Gate::define('delete perfume', fn () => false);
$perfume = Perfume::factory()->create();
// Test delete
mountPerfumeIndexComponent($this->user)
->call('delete', $perfume)
->assertForbidden();
});
it('handles user with partial permissions correctly', function () {
// Remove owner role and create a user with limited permissions
$this->user->roles()->detach();
$limitedRole = Role::create(['name' => 'Limited User']);
$this->user->roles()->attach($limitedRole->id);
// Give only some permissions but not delete
$limitedRole->givePermissionTo([]);
$perfume = Perfume::factory()->create();
// Should not be able to delete
mountPerfumeIndexComponent($this->user)
->call('delete', $perfume)
->assertForbidden();
});

View File

@ -1,473 +0,0 @@
<?php
use App\Enums\AuditStockStatus;
use App\Models\Employee;
use App\Models\Outlet;
use App\Models\Product;
use App\Models\User;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Spatie\Activitylog\Models\Activity;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::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);
// Create test outlets
$this->outlets = Outlet::factory()->count(3)->create();
});
function mountProductAuditComponent(User $user, Outlet $outlet, Product $product)
{
return Livewire::actingAs($user)->test(\App\Livewire\Studio\Catalog\Product\Audit::class, ['outlet' => $outlet, 'product' => $product]);
}
/*
|--------------------------------------------------------------------------
| Audit Component Rendering & Mount
|--------------------------------------------------------------------------
*/
it('renders audit page successfully', function () {
$product = Product::factory()->create();
$outlet = $this->outlets->first();
mountProductAuditComponent($this->user, $outlet, $product)
->assertViewIs('components.sections.ui.audit')
->assertViewHas('pageTitle', "Audit {$product->name} di {$outlet->name}")
->assertViewHas('module', 'product');
});
it('sets label correctly on mount', function () {
$product = Product::factory()->create(['name' => 'Test Product']);
$outlet = $this->outlets->first();
$component = mountProductAuditComponent($this->user, $outlet, $product);
expect($component->label)->toBe('Test Product di '.$outlet->name);
});
/*
|--------------------------------------------------------------------------
| Activity Logs Display Tests
|--------------------------------------------------------------------------
*/
it('displays activity logs correctly', function () {
$product = Product::factory()->create();
$outlet = $this->outlets->first();
// Create some activity logs with expected properties
Activity::create([
'log_name' => 'default',
'description' => 'Stock updated',
'subject_type' => get_class($product),
'subject_id' => $product->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 5,
'quantity_change' => 10,
'new_stock' => 15,
],
'event' => AuditStockStatus::INCREASED->value,
]);
Activity::create([
'log_name' => 'default',
'description' => 'Stock decreased',
'subject_type' => get_class($product),
'subject_id' => $product->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 15,
'quantity_change' => -5,
'new_stock' => 10,
],
'event' => AuditStockStatus::DECREASED->value,
]);
$component = mountProductAuditComponent($this->user, $outlet, $product);
expect($component->activityLogs)->toHaveCount(2);
expect($component->activityLogs->first()->event['label'])->toBe(AuditStockStatus::INCREASED->label());
expect($component->activityLogs->first()->event['color'])->toBe(AuditStockStatus::INCREASED->color());
expect($component->activityLogs->last()->event['label'])->toBe(AuditStockStatus::DECREASED->label());
expect($component->activityLogs->last()->event['color'])->toBe(AuditStockStatus::DECREASED->color());
});
it('filters activity logs by outlet', function () {
$product = Product::factory()->create();
$outlet1 = $this->outlets->first();
$outlet2 = $this->outlets->last();
// Create activity for outlet1
Activity::create([
'log_name' => 'default',
'description' => 'Stock updated outlet1',
'subject_type' => get_class($product),
'subject_id' => $product->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet1->id,
'previous_stock' => 5,
'quantity_change' => 10,
'new_stock' => 15,
],
'event' => AuditStockStatus::UPDATED->value,
]);
// Create activity for outlet2
Activity::create([
'log_name' => 'default',
'description' => 'Stock increased outlet2',
'subject_type' => get_class($product),
'subject_id' => $product->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet2->id,
'previous_stock' => 0,
'quantity_change' => 20,
'new_stock' => 20,
],
'event' => AuditStockStatus::INCREASED->value,
]);
$component1 = mountProductAuditComponent($this->user, $outlet1, $product);
$component2 = mountProductAuditComponent($this->user, $outlet2, $product);
expect($component1->activityLogs)->toHaveCount(1);
expect($component1->activityLogs->first()->properties['outlet_id'])->toBe($outlet1->id);
expect($component2->activityLogs)->toHaveCount(1);
expect($component2->activityLogs->first()->properties['outlet_id'])->toBe($outlet2->id);
});
it('orders activity logs by latest first', function () {
$product = Product::factory()->create();
$outlet = $this->outlets->first();
// Create activities with different timestamps
$activity1 = Activity::create([
'log_name' => 'default',
'description' => 'first activity',
'subject_type' => get_class($product),
'subject_id' => $product->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 5,
'quantity_change' => 5,
'new_stock' => 10,
],
'event' => AuditStockStatus::UPDATED->value,
'created_at' => now()->subMinutes(5),
]);
$activity2 = Activity::create([
'log_name' => 'default',
'description' => 'second activity',
'subject_type' => get_class($product),
'subject_id' => $product->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 10,
'quantity_change' => 10,
'new_stock' => 20,
],
'event' => AuditStockStatus::INCREASED->value,
'created_at' => now(),
]);
$component = mountProductAuditComponent($this->user, $outlet, $product);
expect($component->activityLogs)->toHaveCount(2);
expect($component->activityLogs->first()->id)->toBe($activity2->id);
expect($component->activityLogs->last()->id)->toBe($activity1->id);
});
/*
|--------------------------------------------------------------------------
| Activity Logs Content Tests
|--------------------------------------------------------------------------
*/
it('maps activity events to correct labels and colors', function () {
$product = Product::factory()->create();
$outlet = $this->outlets->first();
// Test INCREASED event
Activity::create([
'log_name' => 'default',
'description' => 'Stock increased',
'subject_type' => get_class($product),
'subject_id' => $product->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 10,
'quantity_change' => 5,
'new_stock' => 15,
],
'event' => AuditStockStatus::INCREASED->value,
]);
// Test DECREASED event
Activity::create([
'log_name' => 'default',
'description' => 'Stock decreased',
'subject_type' => get_class($product),
'subject_id' => $product->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 15,
'quantity_change' => -3,
'new_stock' => 12,
],
'event' => AuditStockStatus::DECREASED->value,
]);
// Test UPDATED event
Activity::create([
'log_name' => 'default',
'description' => 'Stock updated',
'subject_type' => get_class($product),
'subject_id' => $product->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 12,
'quantity_change' => 0,
'new_stock' => 12,
],
'event' => AuditStockStatus::UPDATED->value,
]);
$component = mountProductAuditComponent($this->user, $outlet, $product);
expect($component->activityLogs)->toHaveCount(3);
// Check all event mappings
foreach ($component->activityLogs as $log) {
match ($log->event['label']) {
AuditStockStatus::INCREASED->label() => expect($log->event['color'])->toBe(AuditStockStatus::INCREASED->color()),
AuditStockStatus::DECREASED->label() => expect($log->event['color'])->toBe(AuditStockStatus::DECREASED->color()),
AuditStockStatus::UPDATED->label() => expect($log->event['color'])->toBe(AuditStockStatus::UPDATED->color()),
default => throw new \Exception("Unexpected event label: {$log->event['label']}")
};
}
});
/*
|--------------------------------------------------------------------------
| Empty State Tests
|--------------------------------------------------------------------------
*/
it('handles empty activity logs in audit component', function () {
$product = Product::factory()->create();
$outlet = $this->outlets->first();
$component = mountProductAuditComponent($this->user, $outlet, $product);
expect($component->activityLogs)->toBeInstanceOf(\Illuminate\Database\Eloquent\Collection::class);
expect($component->activityLogs)->toHaveCount(0);
});
it('handles product with no activities in specific outlet', function () {
$product = Product::factory()->create();
$outlet1 = $this->outlets->first();
$outlet2 = $this->outlets->last();
// Create activity only for outlet2
Activity::create([
'log_name' => 'default',
'description' => 'Stock updated',
'subject_type' => get_class($product),
'subject_id' => $product->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet2->id,
'previous_stock' => 5,
'quantity_change' => 5,
'new_stock' => 10,
],
'event' => AuditStockStatus::UPDATED->value,
]);
$component1 = mountProductAuditComponent($this->user, $outlet1, $product);
$component2 = mountProductAuditComponent($this->user, $outlet2, $product);
expect($component1->activityLogs)->toHaveCount(0);
expect($component2->activityLogs)->toHaveCount(1);
});
/*
|--------------------------------------------------------------------------
| Integration Tests
|--------------------------------------------------------------------------
*/
it('displays real activity log data correctly', function () {
$product = Product::factory()->create(['name' => 'Integration Product']);
$outlet = $this->outlets->first();
$activity = Activity::create([
'log_name' => 'default',
'description' => 'Stock adjustment for integration test',
'subject_type' => get_class($product),
'subject_id' => $product->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 100,
'quantity_change' => 25,
'new_stock' => 125,
],
'event' => AuditStockStatus::INCREASED->value,
]);
$component = mountProductAuditComponent($this->user, $outlet, $product);
expect($component->activityLogs)->toHaveCount(1);
$log = $component->activityLogs->first();
expect($log->description)->toBe('Stock adjustment for integration test');
expect($log->properties['previous_stock'])->toBe(100);
expect($log->properties['quantity_change'])->toBe(25);
expect($log->properties['new_stock'])->toBe(125);
expect($log->event['label'])->toBe(AuditStockStatus::INCREASED->label());
expect($log->event['color'])->toBe(AuditStockStatus::INCREASED->color());
});
it('handles multiple activities across different outlets', function () {
$product = Product::factory()->create();
$outlet1 = $this->outlets->first();
$outlet2 = $this->outlets->last();
// Create activities for both outlets
Activity::create([
'log_name' => 'default',
'description' => 'Activity in outlet 1',
'subject_type' => get_class($product),
'subject_id' => $product->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet1->id,
'previous_stock' => 10,
'quantity_change' => 5,
'new_stock' => 15,
],
'event' => AuditStockStatus::INCREASED->value,
]);
Activity::create([
'log_name' => 'default',
'description' => 'Activity in outlet 2',
'subject_type' => get_class($product),
'subject_id' => $product->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet2->id,
'previous_stock' => 20,
'quantity_change' => -3,
'new_stock' => 17,
],
'event' => AuditStockStatus::DECREASED->value,
]);
$component1 = mountProductAuditComponent($this->user, $outlet1, $product);
$component2 = mountProductAuditComponent($this->user, $outlet2, $product);
expect($component1->activityLogs)->toHaveCount(1);
expect($component1->activityLogs->first()->description)->toBe('Activity in outlet 1');
expect($component2->activityLogs)->toHaveCount(1);
expect($component2->activityLogs->first()->description)->toBe('Activity in outlet 2');
});
/*
|--------------------------------------------------------------------------
| Edge Cases and Error Handling
|--------------------------------------------------------------------------
*/
it('handles activity logs with missing properties gracefully', function () {
$product = Product::factory()->create();
$outlet = $this->outlets->first();
// Create activity with incomplete properties (shouldn't happen in real usage)
Activity::create([
'log_name' => 'default',
'description' => 'Incomplete activity',
'subject_type' => get_class($product),
'subject_id' => $product->id,
'causer_type' => get_class($this->user),
'causer_id' => $this->user->id,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 0, // Add required properties
'quantity_change' => 0,
'new_stock' => 0,
],
'event' => AuditStockStatus::UPDATED->value,
]);
$component = mountProductAuditComponent($this->user, $outlet, $product);
expect($component->activityLogs)->toHaveCount(1);
// Component should render correctly with all required properties
});
it('handles activities with null causer', function () {
$product = Product::factory()->create();
$outlet = $this->outlets->first();
Activity::create([
'log_name' => 'default',
'description' => 'Activity with no causer',
'subject_type' => get_class($product),
'subject_id' => $product->id,
'causer_type' => null,
'causer_id' => null,
'properties' => [
'outlet_id' => $outlet->id,
'previous_stock' => 5,
'quantity_change' => 5,
'new_stock' => 10,
],
'event' => AuditStockStatus::INCREASED->value,
]);
$component = mountProductAuditComponent($this->user, $outlet, $product);
expect($component->activityLogs)->toHaveCount(1);
expect($component->activityLogs->first()->causer)->toBeNull();
});

View File

@ -1,351 +0,0 @@
<?php
use App\Models\Employee;
use App\Models\Outlet;
use App\Models\Product;
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);
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);
// Create test outlets
$this->outlets = Outlet::factory()->count(3)->create();
});
function mountProductCreateComponent(User $user)
{
return Livewire::actingAs($user)->test(\App\Livewire\Studio\Catalog\Product\Create::class);
}
/*
|--------------------------------------------------------------------------
| Create Component Rendering & Mount
|--------------------------------------------------------------------------
*/
it('renders create page successfully', function () {
mountProductCreateComponent($this->user)
->assertViewIs('livewire.studio.catalog.product.form')
->assertViewHas('pageTitle', 'Tambah Produk');
});
it('initializes form properties on mount', function () {
$component = mountProductCreateComponent($this->user);
expect($component->form->name)->toBe('');
expect($component->form->sku)->toBe('');
expect($component->form->cost_price)->toBe('');
expect($component->form->sale_price)->toBe('');
expect($component->form->description)->toBeNull();
expect($component->form->outlet_ids)->toBe([]);
expect($component->form->image)->toBe([]);
expect($component->outlets)->toHaveCount(3);
});
/*
|--------------------------------------------------------------------------
| Create Product Tests
|--------------------------------------------------------------------------
*/
it('creates product successfully when authorized', function () {
$data = [
'name' => 'New Product',
'sku' => 'NP001',
'cost_price' => '10000',
'sale_price' => '15000',
'description' => 'Product description',
'outlet_ids' => [$this->outlets->first()->id, $this->outlets->last()->id],
];
mountProductCreateComponent($this->user)
->set('form.name', $data['name'])
->set('form.sku', $data['sku'])
->set('form.cost_price', $data['cost_price'])
->set('form.sale_price', $data['sale_price'])
->set('form.description', $data['description'])
->set('form.outlet_ids', $data['outlet_ids'])
->call('save')
->assertHasNoErrors()
->assertRedirect(route('studio.catalog.product.index'));
expect(Product::count())->toBe(1);
$product = Product::first();
expect($product->name)->toBe($data['name']);
expect($product->sku)->toBe($data['sku']);
expect($product->cost_price)->toBe(10000);
expect($product->sale_price)->toBe(15000);
expect($product->description)->toBe($data['description']);
expect($product->outlets)->toHaveCount(2);
});
it('prevents product creation when unauthorized', function () {
$this->ownerRole->revokePermissionTo('create product');
Gate::define('create product', fn () => false);
mountProductCreateComponent($this->user)
->set('form.name', 'Test Product')
->set('form.sku', 'TP001')
->set('form.sale_price', '10000')
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertForbidden();
});
it('creates product with empty cost_price for non-owner roles', function () {
// Create user with limited role
$this->user->roles()->detach();
$limitedRole = Role::create(['name' => 'Cashier']);
$this->user->roles()->attach($limitedRole->id);
$createPermission = Permission::where('name', 'create product')->first();
$limitedRole->givePermissionTo($createPermission);
mountProductCreateComponent($this->user)
->set('form.name', 'Test Product')
->set('form.sku', 'TP001')
->set('form.cost_price', '') // Empty cost price
->set('form.sale_price', '10000')
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasNoErrors();
$product = Product::first();
expect($product->cost_price)->toBe(0);
});
/*
|--------------------------------------------------------------------------
| Form Validation Tests
|--------------------------------------------------------------------------
*/
it('validates required name field on create', function () {
mountProductCreateComponent($this->user)
->set('form.name', '')
->set('form.sku', 'TP001')
->set('form.sale_price', '10000')
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasErrors(['form.name']);
});
it('validates name field max length on create', function () {
mountProductCreateComponent($this->user)
->set('form.name', str_repeat('a', 51))
->set('form.sku', 'TP001')
->set('form.sale_price', '10000')
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasErrors(['form.name']);
});
it('validates required sku field on create', function () {
mountProductCreateComponent($this->user)
->set('form.name', 'Test Product')
->set('form.sku', '')
->set('form.sale_price', '10000')
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasErrors(['form.sku']);
});
it('validates sku field max length on create', function () {
mountProductCreateComponent($this->user)
->set('form.name', 'Test Product')
->set('form.sku', str_repeat('a', 21))
->set('form.sale_price', '10000')
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasErrors(['form.sku']);
});
it('validates sku uniqueness on create', function () {
Product::factory()->create(['sku' => 'EXISTING001']);
mountProductCreateComponent($this->user)
->set('form.name', 'Test Product')
->set('form.sku', 'EXISTING001')
->set('form.sale_price', '10000')
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasErrors(['form.sku']);
});
it('validates required sale_price field on create', function () {
mountProductCreateComponent($this->user)
->set('form.name', 'Test Product')
->set('form.sku', 'TP001')
->set('form.sale_price', '')
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasErrors(['form.sale_price']);
});
it('validates required outlet_ids field on create', function () {
mountProductCreateComponent($this->user)
->set('form.name', 'Test Product')
->set('form.sku', 'TP001')
->set('form.sale_price', '10000')
->set('form.outlet_ids', [])
->call('save')
->assertHasErrors(['form.outlet_ids']);
});
it('validates outlet_ids contains valid outlet ids on create', function () {
mountProductCreateComponent($this->user)
->set('form.name', 'Test Product')
->set('form.sku', 'TP001')
->set('form.sale_price', '10000')
->set('form.outlet_ids', [999])
->call('save')
->assertHasErrors(['form.outlet_ids.*']);
});
/*
|--------------------------------------------------------------------------
| Real-time Validation Tests
|--------------------------------------------------------------------------
*/
it('validates name field in real-time', function () {
$component = mountProductCreateComponent($this->user);
// Valid name
$component->set('form.name', 'Valid Product Name')
->assertHasNoErrors(['form.name']);
// Empty name
$component->set('form.name', '')
->assertHasErrors(['form.name']);
// Name too long
$component->set('form.name', str_repeat('a', 51))
->assertHasErrors(['form.name']);
});
it('validates sku field in real-time', function () {
$component = mountProductCreateComponent($this->user);
// Valid SKU
$component->set('form.sku', 'VALID001')
->assertHasNoErrors(['form.sku']);
// Empty SKU
$component->set('form.sku', '')
->assertHasErrors(['form.sku']);
// SKU too long
$component->set('form.sku', str_repeat('a', 21))
->assertHasErrors(['form.sku']);
// Duplicate SKU
Product::factory()->create(['sku' => 'EXISTING001']);
$component->set('form.sku', 'EXISTING001')
->assertHasErrors(['form.sku']);
});
it('validates price fields in real-time', function () {
$component = mountProductCreateComponent($this->user);
// Valid sale price
$component->set('form.sale_price', '10000')
->assertHasNoErrors(['form.sale_price']);
// Empty sale price
$component->set('form.sale_price', '')
->assertHasErrors(['form.sale_price']);
// Negative sale price
$component->set('form.sale_price', '-10000')
->assertHasErrors(['form.sale_price']);
// Valid cost price
$component->set('form.cost_price', '5000')
->assertHasNoErrors(['form.cost_price']);
// Negative cost price
$component->set('form.cost_price', '-5000')
->assertHasErrors(['form.cost_price']);
});
it('validates outlet_ids in real-time', function () {
$component = mountProductCreateComponent($this->user);
// Valid outlet IDs
$component->set('form.name', 'Test Product')
->set('form.sku', 'TP001')
->set('form.sale_price', '10000')
->set('form.outlet_ids', [$this->outlets->first()->id])
->assertHasNoErrors(['form.outlet_ids']);
// Empty outlet IDs - should trigger validation on submit
$component->set('form.outlet_ids', [])
->call('save')
->assertHasErrors(['form.outlet_ids']);
// Invalid outlet ID
$component = mountProductCreateComponent($this->user);
$component->set('form.name', 'Test Product')
->set('form.sku', 'TP002')
->set('form.sale_price', '10000')
->set('form.outlet_ids', [999])
->call('save')
->assertHasErrors(['form.outlet_ids.*']);
});
/*
|--------------------------------------------------------------------------
| Integration Tests
|--------------------------------------------------------------------------
*/
it('creates product and attaches to outlets correctly', function () {
$outlet1 = $this->outlets->first();
$outlet2 = $this->outlets->last();
mountProductCreateComponent($this->user)
->set('form.name', 'Integration Test Product')
->set('form.sku', 'ITP001')
->set('form.cost_price', '15000') // Required for Owner role
->set('form.sale_price', '20000')
->set('form.outlet_ids', [$outlet1->id, $outlet2->id])
->call('save')
->assertHasNoErrors();
$product = Product::where('sku', 'ITP001')->first();
expect($product)->not->toBeNull();
expect($product->outlets)->toHaveCount(2);
expect($product->outlets->pluck('id')->toArray())->toContain($outlet1->id, $outlet2->id);
});
it('handles large numbers in price fields', function () {
mountProductCreateComponent($this->user)
->set('form.name', 'Expensive Product')
->set('form.sku', 'EP001')
->set('form.cost_price', '10000000') // 10 million
->set('form.sale_price', '15000000') // 15 million
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasNoErrors();
$product = Product::where('sku', 'EP001')->first();
expect($product->cost_price)->toBe(10000000);
expect($product->sale_price)->toBe(15000000);
});

View File

@ -1,359 +0,0 @@
<?php
use App\Models\Employee;
use App\Models\Outlet;
use App\Models\Product;
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);
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);
// Create test outlets
$this->outlets = Outlet::factory()->count(3)->create();
});
function mountProductEditComponent(User $user, Product $product)
{
return Livewire::actingAs($user)->test(\App\Livewire\Studio\Catalog\Product\Edit::class, ['product' => $product->hash]);
}
/*
|--------------------------------------------------------------------------
| Edit Component Rendering & Mount
|--------------------------------------------------------------------------
*/
it('renders edit page successfully', function () {
$product = Product::factory()->create();
mountProductEditComponent($this->user, $product)
->assertViewIs('livewire.studio.catalog.product.form')
->assertViewHas('pageTitle', 'Ubah Produk');
});
it('populates form with product data on mount', function () {
$product = Product::factory()->create([
'name' => 'Test Product',
'sku' => 'TP001',
'cost_price' => 10000,
'sale_price' => 15000,
'description' => 'Test description',
]);
$product->outlets()->attach([$this->outlets->first()->id]);
$component = mountProductEditComponent($this->user, $product);
expect($component->form->name)->toBe('Test Product');
expect($component->form->sku)->toBe('TP001');
expect($component->form->cost_price)->toBe('10000');
expect($component->form->sale_price)->toBe('15000');
expect($component->form->description)->toBe('Test description');
expect($component->form->outlet_ids)->toBe([$this->outlets->first()->id]);
});
/*
|--------------------------------------------------------------------------
| Update Product Tests
|--------------------------------------------------------------------------
*/
it('updates product successfully when authorized', function () {
$updatePermission = Permission::where('name', 'update product')->first();
$this->user->givePermissionTo($updatePermission);
$product = Product::factory()->create([
'name' => 'Original Product',
'sku' => 'OP001',
'cost_price' => 10000,
'sale_price' => 15000,
]);
$product->outlets()->attach([$this->outlets->first()->id]);
$updateData = [
'name' => 'Updated Product',
'sku' => 'UP001',
'cost_price' => '12000',
'sale_price' => '18000',
'description' => 'Updated description',
'outlet_ids' => [$this->outlets->first()->id, $this->outlets->last()->id],
];
mountProductEditComponent($this->user, $product)
->set('form.name', $updateData['name'])
->set('form.sku', $updateData['sku'])
->set('form.cost_price', $updateData['cost_price'])
->set('form.sale_price', $updateData['sale_price'])
->set('form.description', $updateData['description'])
->set('form.outlet_ids', $updateData['outlet_ids'])
->call('save')
->assertHasNoErrors()
->assertRedirect(route('studio.catalog.product.index'));
$product->refresh();
expect($product->name)->toBe($updateData['name']);
expect($product->sku)->toBe($updateData['sku']);
expect($product->cost_price)->toBe(12000);
expect($product->sale_price)->toBe(18000);
expect($product->description)->toBe($updateData['description']);
expect($product->outlets)->toHaveCount(2);
});
it('prevents product update when unauthorized', function () {
$this->ownerRole->revokePermissionTo('update product');
Gate::define('update product', fn () => false);
$product = Product::factory()->create();
mountProductEditComponent($this->user, $product)
->set('form.name', 'Updated Name')
->call('save')
->assertForbidden();
});
it('syncs outlet relationships correctly on update', function () {
$updatePermission = Permission::where('name', 'update product')->first();
$this->user->givePermissionTo($updatePermission);
$product = Product::factory()->create();
$product->outlets()->attach([$this->outlets->first()->id]);
// Change to different outlets
mountProductEditComponent($this->user, $product)
->set('form.name', 'Updated Product')
->set('form.sku', 'UP001')
->set('form.sale_price', '10000')
->set('form.outlet_ids', [$this->outlets[1]->id, $this->outlets[2]->id])
->call('save')
->assertHasNoErrors();
$product->refresh();
expect($product->outlets)->toHaveCount(2);
expect($product->outlets->pluck('id')->toArray())->toBe([$this->outlets[1]->id, $this->outlets[2]->id]);
});
it('allows same sku on update of same product', function () {
$updatePermission = Permission::where('name', 'update product')->first();
$this->user->givePermissionTo($updatePermission);
$product = Product::factory()->create(['sku' => 'EXISTING001']);
mountProductEditComponent($this->user, $product)
->set('form.name', 'Updated Product')
->set('form.sku', 'EXISTING001') // Same SKU as existing product
->set('form.sale_price', '10000')
->set('form.outlet_ids', [$this->outlets->first()->id])
->call('save')
->assertHasNoErrors();
});
/*
|--------------------------------------------------------------------------
| Form Validation Tests on Edit
|--------------------------------------------------------------------------
*/
it('validates required name field on update', function () {
$product = Product::factory()->create();
mountProductEditComponent($this->user, $product)
->set('form.name', '')
->call('save')
->assertHasErrors(['form.name']);
});
it('validates name field max length on update', function () {
$product = Product::factory()->create();
mountProductEditComponent($this->user, $product)
->set('form.name', str_repeat('a', 51))
->call('save')
->assertHasErrors(['form.name']);
});
it('validates required sku field on update', function () {
$product = Product::factory()->create();
mountProductEditComponent($this->user, $product)
->set('form.sku', '')
->call('save')
->assertHasErrors(['form.sku']);
});
it('validates sku field max length on update', function () {
$product = Product::factory()->create();
mountProductEditComponent($this->user, $product)
->set('form.sku', str_repeat('a', 21))
->call('save')
->assertHasErrors(['form.sku']);
});
it('validates sku uniqueness on update with different product', function () {
$product1 = Product::factory()->create(['sku' => 'SKU001']);
$product2 = Product::factory()->create(['sku' => 'SKU002']);
mountProductEditComponent($this->user, $product1)
->set('form.sku', 'SKU002') // Try to use SKU from product2
->call('save')
->assertHasErrors(['form.sku']);
});
it('validates required sale_price field on update', function () {
$product = Product::factory()->create();
mountProductEditComponent($this->user, $product)
->set('form.sale_price', '')
->call('save')
->assertHasErrors(['form.sale_price']);
});
it('validates required outlet_ids field on update', function () {
$product = Product::factory()->create();
mountProductEditComponent($this->user, $product)
->set('form.outlet_ids', [])
->call('save')
->assertHasErrors(['form.outlet_ids']);
});
it('validates outlet_ids contains valid outlet ids on update', function () {
$product = Product::factory()->create();
mountProductEditComponent($this->user, $product)
->set('form.outlet_ids', [999])
->call('save')
->assertHasErrors(['form.outlet_ids.*']);
});
/*
|--------------------------------------------------------------------------
| Real-time Validation Tests on Edit
|--------------------------------------------------------------------------
*/
it('validates name field in real-time on edit', function () {
$product = Product::factory()->create();
$component = mountProductEditComponent($this->user, $product);
// Valid name
$component->set('form.name', 'Valid Updated Product Name')
->assertHasNoErrors(['form.name']);
// Empty name
$component->set('form.name', '')
->assertHasErrors(['form.name']);
// Name too long
$component->set('form.name', str_repeat('a', 51))
->assertHasErrors(['form.name']);
});
it('validates sku field in real-time on edit', function () {
$product = Product::factory()->create();
$component = mountProductEditComponent($this->user, $product);
// Valid SKU
$component->set('form.sku', 'VALID001')
->assertHasNoErrors(['form.sku']);
// Empty SKU
$component->set('form.sku', '')
->assertHasErrors(['form.sku']);
// SKU too long
$component->set('form.sku', str_repeat('a', 21))
->assertHasErrors(['form.sku']);
});
it('validates price fields in real-time on edit', function () {
$product = Product::factory()->create();
$component = mountProductEditComponent($this->user, $product);
// Valid sale price
$component->set('form.sale_price', '10000')
->assertHasNoErrors(['form.sale_price']);
// Empty sale price
$component->set('form.sale_price', '')
->assertHasErrors(['form.sale_price']);
// Negative sale price
$component->set('form.sale_price', '-10000')
->assertHasErrors(['form.sale_price']);
});
/*
|--------------------------------------------------------------------------
| Integration Tests
|--------------------------------------------------------------------------
*/
it('updates product and syncs outlet relationships', function () {
$updatePermission = Permission::where('name', 'update product')->first();
$this->user->givePermissionTo($updatePermission);
$product = Product::factory()->create();
$product->outlets()->attach([$this->outlets->first()->id]);
$newOutlets = [$this->outlets[1]->id, $this->outlets[2]->id];
mountProductEditComponent($this->user, $product)
->set('form.name', 'Updated Integration Product')
->set('form.sku', 'UIP001')
->set('form.sale_price', '25000')
->set('form.outlet_ids', $newOutlets)
->call('save')
->assertHasNoErrors();
$product->refresh();
expect($product->name)->toBe('Updated Integration Product');
expect($product->outlets)->toHaveCount(2);
expect($product->outlets->pluck('id')->toArray())->toBe($newOutlets);
});
/*
|--------------------------------------------------------------------------
| Error Handling and Edge Cases
|--------------------------------------------------------------------------
*/
it('handles invalid product hash in edit component', function () {
expect(fn () => mountProductEditComponent($this->user, 'invalid-hash'))
->toThrow(\TypeError::class);
});
it('validates form data integrity during operations', function () {
// Test that form data is properly reset between operations
$product1 = Product::factory()->create(['name' => 'First Category']);
$product2 = Product::factory()->create(['name' => 'Second Category']);
$component = mountProductEditComponent($this->user, $product1);
// Open modal for first category
expect($component->form->name)->toBe('First Category');
// Verify first category wasn't modified
$product1->refresh();
expect($product1->name)->toBe('First Category');
});

View File

@ -1,120 +0,0 @@
<?php
use App\Models\Employee;
use App\Models\Product;
use App\Models\User;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::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 mountProductIndexComponent(User $user)
{
return Livewire::actingAs($user)->test(\App\Livewire\Studio\Catalog\Product\Index::class);
}
/*
|--------------------------------------------------------------------------
| Index Component Rendering & Basic Functionality
|--------------------------------------------------------------------------
*/
it('renders index page successfully', function () {
mountProductIndexComponent($this->user)
->assertViewIs('livewire.studio.catalog.product.index')
->assertViewHas('pageTitle', 'Produk');
});
it('displays products correctly', function () {
Product::factory()->count(3)->create();
// Just check that the component renders without errors and has products
$component = mountProductIndexComponent($this->user);
$component->assertViewIs('livewire.studio.catalog.product.index');
expect(Product::count())->toBe(3);
});
/*
|--------------------------------------------------------------------------
| Delete Product Tests
|--------------------------------------------------------------------------
*/
it('deletes product successfully when authorized', function () {
$product = Product::factory()->create([
'name' => 'Product to Delete',
]);
mountProductIndexComponent($this->user)
->call('delete', $product)
->assertHasNoErrors()
->assertDispatched('refreshDatatable');
$this->assertSoftDeleted('products', [
'id' => $product->id,
]);
});
it('prevents product deletion when unauthorized', function () {
$this->ownerRole->revokePermissionTo('delete product');
Gate::define('delete product', fn () => false);
$product = Product::factory()->create();
mountProductIndexComponent($this->user)
->call('delete', $product)
->assertForbidden();
});
/*
|--------------------------------------------------------------------------
| Authorization Edge Cases
|--------------------------------------------------------------------------
*/
it('handles unauthorized user with revoked permissions', function () {
// Revoke all product permissions
$this->ownerRole->revokePermissionTo(['delete product']);
// Define gates to return false
Gate::define('delete product', fn () => false);
$product = Product::factory()->create();
// Test delete
mountProductIndexComponent($this->user)
->call('delete', $product)
->assertForbidden();
});
it('handles user with partial permissions correctly', function () {
// Remove owner role and create a user with limited permissions
$this->user->roles()->detach();
$limitedRole = Role::create(['name' => 'Limited User']);
$this->user->roles()->attach($limitedRole->id);
// Give only some permissions but not delete
$limitedRole->givePermissionTo([]);
$product = Product::factory()->create();
// Should not be able to delete
mountProductIndexComponent($this->user)
->call('delete', $product)
->assertForbidden();
});

View File

@ -1,431 +0,0 @@
<?php
use App\Livewire\Studio\Feature\ChooseUs;
use App\Models\ChooseUs as ChooseUsModel;
use App\Models\User;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->seed(RolePermissionSeeder::class);
$this->user = User::factory()
->active()
->create();
$this->ownerRole = Role::where('name', 'Owner')->first();
$this->user->roles()->attach($this->ownerRole->id);
});
function mountChooseUsComponent(User $user)
{
return Livewire::actingAs($user)->test(ChooseUs::class);
}
/*
|--------------------------------------------------------------------------
| Component Rendering & Mount
|--------------------------------------------------------------------------
*/
it('renders page successfully', function () {
mountChooseUsComponent($this->user)
->assertViewIs('livewire.studio.feature.choose-us')
->assertViewHas('pageTitle', 'Pilih Kami');
});
it('mounts with default method', function () {
$component = mountChooseUsComponent($this->user);
expect($component->method)->toBe('create');
expect($component->modalTitle)->toBe('');
});
/*
|--------------------------------------------------------------------------
| Create Choose Us
|--------------------------------------------------------------------------
*/
it('creates choose us successfully', function () {
mountChooseUsComponent($this->user)
->set('form.name', 'Quality Products')
->set('form.description', 'We provide high-quality products that meet your needs.')
->call('create');
$chooseUs = ChooseUsModel::first();
expect($chooseUs)->not->toBeNull();
expect($chooseUs->name)->toBe('Quality Products');
expect($chooseUs->description)->toBe('We provide high-quality products that meet your needs.');
expect($chooseUs->sort_order)->toBe(1);
});
it('validates required fields on create', function () {
mountChooseUsComponent($this->user)
->set('form.name', '')
->set('form.description', '')
->call('create')
->assertHasErrors([
'form.name' => 'required',
'form.description' => 'required',
]);
});
it('validates name max length on create', function () {
mountChooseUsComponent($this->user)
->set('form.name', str_repeat('a', 51))
->set('form.description', 'Valid description')
->call('create')
->assertHasErrors([
'form.name' => 'max',
]);
});
it('dispatches refresh datatable event on create', function () {
mountChooseUsComponent($this->user)
->set('form.name', 'Quality Products')
->set('form.description', 'We provide high-quality products.')
->call('create')
->assertDispatched('refreshDatatable');
});
it('shows success toast on create', function () {
// We can't directly test toast messages in Livewire tests
// but we can verify the method completes without errors
mountChooseUsComponent($this->user)
->set('form.name', 'Quality Products')
->set('form.description', 'We provide high-quality products.')
->call('create');
expect(ChooseUsModel::count())->toBe(1);
});
it('sets correct sort order on create', function () {
// Create first item
ChooseUsModel::create([
'name' => 'First Item',
'description' => 'First description',
'sort_order' => 1,
]);
// Create second item
mountChooseUsComponent($this->user)
->set('form.name', 'Second Item')
->set('form.description', 'Second description')
->call('create');
$secondItem = ChooseUsModel::where('name', 'Second Item')->first();
expect($secondItem->sort_order)->toBe(2);
});
/*
|--------------------------------------------------------------------------
| Update Choose Us
|--------------------------------------------------------------------------
*/
it('updates choose us successfully', function () {
$chooseUs = ChooseUsModel::factory()->create([
'name' => 'Old Name',
'description' => 'Old description',
]);
mountChooseUsComponent($this->user)
->set('form.chooseUs', $chooseUs)
->set('form.name', 'Updated Name')
->set('form.description', 'Updated description')
->call('update');
$chooseUs->refresh();
expect($chooseUs->name)->toBe('Updated Name');
expect($chooseUs->description)->toBe('Updated description');
});
it('validates required fields on update', function () {
$chooseUs = ChooseUsModel::factory()->create();
mountChooseUsComponent($this->user)
->set('form.chooseUs', $chooseUs)
->set('form.name', '')
->set('form.description', '')
->call('update')
->assertHasErrors([
'form.name' => 'required',
'form.description' => 'required',
]);
});
it('dispatches refresh datatable event on update', function () {
$chooseUs = ChooseUsModel::factory()->create();
mountChooseUsComponent($this->user)
->set('form.chooseUs', $chooseUs)
->set('form.name', 'Updated Name')
->set('form.description', 'Updated description')
->call('update')
->assertDispatched('refreshDatatable');
});
it('shows success toast on update', function () {
$chooseUs = ChooseUsModel::factory()->create([
'name' => 'Old Name',
'description' => 'Old description',
]);
mountChooseUsComponent($this->user)
->set('form.chooseUs', $chooseUs)
->set('form.name', 'Updated Name')
->set('form.description', 'Updated description')
->call('update');
$chooseUs->refresh();
expect($chooseUs->name)->toBe('Updated Name');
});
/*
|--------------------------------------------------------------------------
| Delete Choose Us
|--------------------------------------------------------------------------
*/
it('deletes choose us successfully', function () {
$chooseUs = ChooseUsModel::factory()->create();
mountChooseUsComponent($this->user)
->call('delete', $chooseUs);
expect(ChooseUsModel::find($chooseUs->id))->toBeNull();
});
it('reorders sort order after delete', function () {
// Create three items
$item1 = ChooseUsModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
$item2 = ChooseUsModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
$item3 = ChooseUsModel::create(['name' => 'Item 3', 'description' => 'Desc 3', 'sort_order' => 3]);
// Delete middle item
mountChooseUsComponent($this->user)
->call('delete', $item2);
// Refresh remaining items
$item1->refresh();
$item3->refresh();
expect($item1->sort_order)->toBe(1);
expect($item3->sort_order)->toBe(2);
});
it('dispatches refresh datatable event on delete', function () {
$chooseUs = ChooseUsModel::factory()->create();
mountChooseUsComponent($this->user)
->call('delete', $chooseUs)
->assertDispatched('refreshDatatable');
});
it('shows success toast on delete', function () {
$chooseUs = ChooseUsModel::factory()->create();
mountChooseUsComponent($this->user)
->call('delete', $chooseUs);
expect(ChooseUsModel::find($chooseUs->id))->toBeNull();
});
/*
|--------------------------------------------------------------------------
| Sorting Functionality
|--------------------------------------------------------------------------
*/
it('moves item up successfully', function () {
$item1 = ChooseUsModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
$item2 = ChooseUsModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
mountChooseUsComponent($this->user)
->call('sortOrder', $item2, 'up');
$item1->refresh();
$item2->refresh();
expect($item1->sort_order)->toBe(2);
expect($item2->sort_order)->toBe(1);
});
it('moves item down successfully', function () {
$item1 = ChooseUsModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
$item2 = ChooseUsModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
mountChooseUsComponent($this->user)
->call('sortOrder', $item1, 'down');
$item1->refresh();
$item2->refresh();
expect($item1->sort_order)->toBe(2);
expect($item2->sort_order)->toBe(1);
});
it('moves item to first position', function () {
$item1 = ChooseUsModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
$item2 = ChooseUsModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
$item3 = ChooseUsModel::create(['name' => 'Item 3', 'description' => 'Desc 3', 'sort_order' => 3]);
mountChooseUsComponent($this->user)
->call('sortOrder', $item3, 'first');
$item1->refresh();
$item2->refresh();
$item3->refresh();
expect($item3->sort_order)->toBe(1);
expect($item1->sort_order)->toBe(2);
expect($item2->sort_order)->toBe(3);
});
it('moves item to last position', function () {
$item1 = ChooseUsModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
$item2 = ChooseUsModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
$item3 = ChooseUsModel::create(['name' => 'Item 3', 'description' => 'Desc 3', 'sort_order' => 3]);
mountChooseUsComponent($this->user)
->call('sortOrder', $item1, 'last');
$item1->refresh();
$item2->refresh();
$item3->refresh();
expect($item1->sort_order)->toBe(3);
expect($item2->sort_order)->toBe(1);
expect($item3->sort_order)->toBe(2);
});
it('dispatches refresh datatable event on sort', function () {
$item1 = ChooseUsModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
$item2 = ChooseUsModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
mountChooseUsComponent($this->user)
->call('sortOrder', $item2, 'up')
->assertDispatched('refreshDatatable');
});
it('does not move first item up', function () {
$item1 = ChooseUsModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
$item2 = ChooseUsModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
mountChooseUsComponent($this->user)
->call('sortOrder', $item1, 'up');
$item1->refresh();
$item2->refresh();
expect($item1->sort_order)->toBe(1);
expect($item2->sort_order)->toBe(2);
});
it('does not move last item down', function () {
$item1 = ChooseUsModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
$item2 = ChooseUsModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
mountChooseUsComponent($this->user)
->call('sortOrder', $item2, 'down');
$item1->refresh();
$item2->refresh();
expect($item1->sort_order)->toBe(1);
expect($item2->sort_order)->toBe(2);
});
/*
|--------------------------------------------------------------------------
| Modal Operations
|--------------------------------------------------------------------------
*/
it('opens modal for create', function () {
$component = mountChooseUsComponent($this->user)
->call('openModal', 'create', 'Tambah Pilih Kami');
expect($component->method)->toBe('create');
expect($component->modalTitle)->toBe('Tambah Pilih Kami');
});
it('opens modal for update', function () {
$chooseUs = ChooseUsModel::factory()->create([
'name' => 'Test Name',
'description' => 'Test Description',
]);
$component = mountChooseUsComponent($this->user)
->call('openModal', 'update', 'Edit Pilih Kami', $chooseUs->hash);
expect($component->method)->toBe('update');
expect($component->modalTitle)->toBe('Edit Pilih Kami');
// Note: We can't easily test the form data setting in this context
});
it('resets validation on modal open', function () {
// This would require a more complex setup to test validation reset
// For now, we'll assume the resetValidation() and resetErrorBag() methods work as expected
expect(true)->toBeTrue();
});
/*
|--------------------------------------------------------------------------
| Authorization Tests
|--------------------------------------------------------------------------
*/
it('requires create choose us permission', function () {
// Create a user without Owner role for this test
$userWithoutPermission = User::factory()->active()->create();
mountChooseUsComponent($userWithoutPermission)
->set('form.name', 'Test Name')
->set('form.description', 'Test Description')
->call('create')
->assertForbidden();
});
it('requires update choose us permission', function () {
// Create a user without Owner role for this test
$userWithoutPermission = User::factory()->active()->create();
$chooseUs = ChooseUsModel::factory()->create();
mountChooseUsComponent($userWithoutPermission)
->set('form.chooseUs', $chooseUs)
->set('form.name', 'Updated Name')
->set('form.description', 'Updated Description')
->call('update')
->assertForbidden();
});
it('requires delete choose us permission', function () {
// Create a user without Owner role for this test
$userWithoutPermission = User::factory()->active()->create();
$chooseUs = ChooseUsModel::factory()->create();
mountChooseUsComponent($userWithoutPermission)
->call('delete', $chooseUs)
->assertForbidden();
});
it('requires update choose us permission for sorting', function () {
// Create a user without Owner role for this test
$userWithoutPermission = User::factory()->active()->create();
$item1 = ChooseUsModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
$item2 = ChooseUsModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
mountChooseUsComponent($userWithoutPermission)
->call('sortOrder', $item2, 'up')
->assertForbidden();
});

View File

@ -1,426 +0,0 @@
<?php
use App\Livewire\Studio\Feature\PerfumeFeature;
use App\Models\PerfumeFeature as PerfumeFeatureModel;
use App\Models\User;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->seed(RolePermissionSeeder::class);
$this->user = User::factory()
->active()
->create();
$this->ownerRole = Role::where('name', 'Owner')->first();
$this->user->roles()->attach($this->ownerRole->id);
});
/*
|--------------------------------------------------------------------------
| Component Rendering & Mount
|--------------------------------------------------------------------------
*/
it('renders page successfully', function () {
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
->assertViewIs('livewire.studio.feature.perfume-features')
->assertViewHas('pageTitle', 'Keunggulan Parfum');
});
it('mounts with default method', function () {
$component = Livewire::actingAs($this->user)->test(PerfumeFeature::class);
expect($component->method)->toBe('create');
expect($component->modalTitle)->toBe('');
});
/*
|--------------------------------------------------------------------------
| Create Perfume Feature
|--------------------------------------------------------------------------
*/
it('creates perfume feature successfully', function () {
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
->set('form.name', 'Aroma Tahan Lama')
->set('form.description', 'Parfum kami memiliki aroma yang tahan lama hingga 24 jam.')
->call('create');
$perfumeFeature = PerfumeFeatureModel::first();
expect($perfumeFeature)->not->toBeNull();
expect($perfumeFeature->name)->toBe('Aroma Tahan Lama');
expect($perfumeFeature->description)->toBe('Parfum kami memiliki aroma yang tahan lama hingga 24 jam.');
expect($perfumeFeature->sort_order)->toBe(1);
});
it('validates required fields on create', function () {
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
->set('form.name', '')
->set('form.description', '')
->call('create')
->assertHasErrors([
'form.name' => 'required',
'form.description' => 'required',
]);
});
it('validates name max length on create', function () {
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
->set('form.name', str_repeat('a', 51))
->set('form.description', 'Valid description')
->call('create')
->assertHasErrors([
'form.name' => 'max',
]);
});
it('dispatches refresh datatable event on create', function () {
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
->set('form.name', 'Aroma Tahan Lama')
->set('form.description', 'Parfum kami memiliki aroma yang tahan lama hingga 24 jam.')
->call('create')
->assertDispatched('refreshDatatable');
});
it('shows success toast on create', function () {
// We can't directly test toast messages in Livewire tests
// but we can verify the method completes without errors
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
->set('form.name', 'Aroma Tahan Lama')
->set('form.description', 'Parfum kami memiliki aroma yang tahan lama hingga 24 jam.')
->call('create');
expect(PerfumeFeatureModel::count())->toBe(1);
});
it('sets correct sort order on create', function () {
// Create first item
PerfumeFeatureModel::create([
'name' => 'First Item',
'description' => 'First description',
'sort_order' => 1,
]);
// Create second item
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
->set('form.name', 'Second Item')
->set('form.description', 'Second description')
->call('create');
$secondItem = PerfumeFeatureModel::where('name', 'Second Item')->first();
expect($secondItem->sort_order)->toBe(2);
});
/*
|--------------------------------------------------------------------------
| Update Perfume Feature
|--------------------------------------------------------------------------
*/
it('updates perfume feature successfully', function () {
$perfumeFeature = PerfumeFeatureModel::factory()->create([
'name' => 'Old Name',
'description' => 'Old description',
]);
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
->set('form.perfumeFeature', $perfumeFeature)
->set('form.name', 'Updated Name')
->set('form.description', 'Updated description')
->call('update');
$perfumeFeature->refresh();
expect($perfumeFeature->name)->toBe('Updated Name');
expect($perfumeFeature->description)->toBe('Updated description');
});
it('validates required fields on update', function () {
$perfumeFeature = PerfumeFeatureModel::factory()->create();
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
->set('form.perfumeFeature', $perfumeFeature)
->set('form.name', '')
->set('form.description', '')
->call('update')
->assertHasErrors([
'form.name' => 'required',
'form.description' => 'required',
]);
});
it('dispatches refresh datatable event on update', function () {
$perfumeFeature = PerfumeFeatureModel::factory()->create();
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
->set('form.perfumeFeature', $perfumeFeature)
->set('form.name', 'Updated Name')
->set('form.description', 'Updated description')
->call('update')
->assertDispatched('refreshDatatable');
});
it('shows success toast on update', function () {
$perfumeFeature = PerfumeFeatureModel::factory()->create([
'name' => 'Old Name',
'description' => 'Old description',
]);
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
->set('form.perfumeFeature', $perfumeFeature)
->set('form.name', 'Updated Name')
->set('form.description', 'Updated description')
->call('update');
$perfumeFeature->refresh();
expect($perfumeFeature->name)->toBe('Updated Name');
});
/*
|--------------------------------------------------------------------------
| Delete Perfume Feature
|--------------------------------------------------------------------------
*/
it('deletes perfume feature successfully', function () {
$perfumeFeature = PerfumeFeatureModel::factory()->create();
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
->call('delete', $perfumeFeature);
expect(PerfumeFeatureModel::find($perfumeFeature->id))->toBeNull();
});
it('reorders sort order after delete', function () {
// Create three items
$item1 = PerfumeFeatureModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
$item2 = PerfumeFeatureModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
$item3 = PerfumeFeatureModel::create(['name' => 'Item 3', 'description' => 'Desc 3', 'sort_order' => 3]);
// Delete middle item
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
->call('delete', $item2);
// Refresh remaining items
$item1->refresh();
$item3->refresh();
expect($item1->sort_order)->toBe(1);
expect($item3->sort_order)->toBe(2);
});
it('dispatches refresh datatable event on delete', function () {
$perfumeFeature = PerfumeFeatureModel::factory()->create();
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
->call('delete', $perfumeFeature)
->assertDispatched('refreshDatatable');
});
it('shows success toast on delete', function () {
$perfumeFeature = PerfumeFeatureModel::factory()->create();
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
->call('delete', $perfumeFeature);
expect(PerfumeFeatureModel::find($perfumeFeature->id))->toBeNull();
});
/*
|--------------------------------------------------------------------------
| Sorting Functionality
|--------------------------------------------------------------------------
*/
it('moves item up successfully', function () {
$item1 = PerfumeFeatureModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
$item2 = PerfumeFeatureModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
->call('sortOrder', $item2, 'up');
$item1->refresh();
$item2->refresh();
expect($item1->sort_order)->toBe(2);
expect($item2->sort_order)->toBe(1);
});
it('moves item down successfully', function () {
$item1 = PerfumeFeatureModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
$item2 = PerfumeFeatureModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
->call('sortOrder', $item1, 'down');
$item1->refresh();
$item2->refresh();
expect($item1->sort_order)->toBe(2);
expect($item2->sort_order)->toBe(1);
});
it('moves item to first position', function () {
$item1 = PerfumeFeatureModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
$item2 = PerfumeFeatureModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
$item3 = PerfumeFeatureModel::create(['name' => 'Item 3', 'description' => 'Desc 3', 'sort_order' => 3]);
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
->call('sortOrder', $item3, 'first');
$item1->refresh();
$item2->refresh();
$item3->refresh();
expect($item3->sort_order)->toBe(1);
expect($item1->sort_order)->toBe(2);
expect($item2->sort_order)->toBe(3);
});
it('moves item to last position', function () {
$item1 = PerfumeFeatureModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
$item2 = PerfumeFeatureModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
$item3 = PerfumeFeatureModel::create(['name' => 'Item 3', 'description' => 'Desc 3', 'sort_order' => 3]);
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
->call('sortOrder', $item1, 'last');
$item1->refresh();
$item2->refresh();
$item3->refresh();
expect($item1->sort_order)->toBe(3);
expect($item2->sort_order)->toBe(1);
expect($item3->sort_order)->toBe(2);
});
it('dispatches refresh datatable event on sort', function () {
$item1 = PerfumeFeatureModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
$item2 = PerfumeFeatureModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
->call('sortOrder', $item2, 'up')
->assertDispatched('refreshDatatable');
});
it('does not move first item up', function () {
$item1 = PerfumeFeatureModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
$item2 = PerfumeFeatureModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
->call('sortOrder', $item1, 'up');
$item1->refresh();
$item2->refresh();
expect($item1->sort_order)->toBe(1);
expect($item2->sort_order)->toBe(2);
});
it('does not move last item down', function () {
$item1 = PerfumeFeatureModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
$item2 = PerfumeFeatureModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
->call('sortOrder', $item2, 'down');
$item1->refresh();
$item2->refresh();
expect($item1->sort_order)->toBe(1);
expect($item2->sort_order)->toBe(2);
});
/*
|--------------------------------------------------------------------------
| Modal Operations
|--------------------------------------------------------------------------
*/
it('opens modal for create', function () {
$component = Livewire::actingAs($this->user)->test(PerfumeFeature::class)
->call('openModal', 'create', 'Tambah Keunggulan Parfum');
expect($component->method)->toBe('create');
expect($component->modalTitle)->toBe('Tambah Keunggulan Parfum');
});
it('opens modal for update', function () {
$perfumeFeature = PerfumeFeatureModel::factory()->create([
'name' => 'Test Name',
'description' => 'Test Description',
]);
$component = Livewire::actingAs($this->user)->test(PerfumeFeature::class)
->call('openModal', 'update', 'Edit Keunggulan Parfum', $perfumeFeature->hash);
expect($component->method)->toBe('update');
expect($component->modalTitle)->toBe('Edit Keunggulan Parfum');
// Note: We can't easily test the form data setting in this context
});
it('resets validation on modal open', function () {
// This would require a more complex setup to test validation reset
// For now, we'll assume the resetValidation() and resetErrorBag() methods work as expected
expect(true)->toBeTrue();
});
/*
|--------------------------------------------------------------------------
| Authorization Tests
|--------------------------------------------------------------------------
*/
it('requires create perfume feature permission', function () {
// Create a user without Owner role for this test
$userWithoutPermission = User::factory()->active()->create();
Livewire::actingAs($userWithoutPermission)->test(PerfumeFeature::class)
->set('form.name', 'Test Name')
->set('form.description', 'Test Description')
->call('create')
->assertForbidden();
});
it('requires update perfume feature permission', function () {
// Create a user without Owner role for this test
$userWithoutPermission = User::factory()->active()->create();
$perfumeFeature = PerfumeFeatureModel::factory()->create();
Livewire::actingAs($userWithoutPermission)->test(PerfumeFeature::class)
->set('form.perfumeFeature', $perfumeFeature)
->set('form.name', 'Updated Name')
->set('form.description', 'Updated Description')
->call('update')
->assertForbidden();
});
it('requires delete perfume feature permission', function () {
// Create a user without Owner role for this test
$userWithoutPermission = User::factory()->active()->create();
$perfumeFeature = PerfumeFeatureModel::factory()->create();
Livewire::actingAs($userWithoutPermission)->test(PerfumeFeature::class)
->call('delete', $perfumeFeature)
->assertForbidden();
});
it('requires update perfume feature permission for sorting', function () {
// Create a user without Owner role for this test
$userWithoutPermission = User::factory()->active()->create();
$item1 = PerfumeFeatureModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
$item2 = PerfumeFeatureModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
Livewire::actingAs($userWithoutPermission)->test(PerfumeFeature::class)
->call('sortOrder', $item2, 'up')
->assertForbidden();
});

View File

@ -1,246 +0,0 @@
<?php
use App\Livewire\Studio\Finance\Expense;
use App\Models\Employee;
use App\Models\Expense as ExpenseModel;
use App\Models\Outlet;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
uses(RefreshDatabase::class);
beforeEach(function () {
// Setup Permissions
Permission::create(['name' => 'create expense']);
Permission::create(['name' => 'update expense']);
Permission::create(['name' => 'delete expense']);
});
function createUserWithOutlet(bool $multipleOutlets = false)
{
$user = User::factory()
->has(Employee::factory(), 'employee')
->create();
$outlet1 = Outlet::factory()->create();
$user->outlets()->attach($outlet1);
if ($multipleOutlets) {
$outlet2 = Outlet::factory()->create();
$user->outlets()->attach($outlet2);
}
// Assign permissions
$user->givePermissionTo(['create expense', 'update expense', 'delete expense']);
return [$user, $outlet1];
}
it('renders successfully', function () {
[$user, $outlet] = createUserWithOutlet();
$this->actingAs($user);
Livewire::test(Expense::class)
->assertStatus(200);
});
it('validates required fields', function () {
[$user, $outlet] = createUserWithOutlet();
$this->actingAs($user);
Livewire::test(Expense::class)
->set('form.description', '')
->set('form.amount', '')
->call('create')
->assertHasErrors(['form.description', 'form.amount']);
});
it('validates amount must be numeric', function () {
[$user, $outlet] = createUserWithOutlet();
$this->actingAs($user);
Livewire::test(Expense::class)
->set('form.amount', 'abc')
->call('create')
->assertHasErrors(['form.amount']);
});
it('validates outlet_id is required for multi-outlet users', function () {
[$user, $outlet] = createUserWithOutlet(true); // Multiple outlets
$this->actingAs($user);
Livewire::test(Expense::class)
->set('form.description', 'Test Expense')
->set('form.amount', '50000')
// outlet_id not set
->call('create')
->assertHasErrors(['form.outlet_id']);
});
it('does not require manually selecting outlet_id for single-outlet users', function () {
[$user, $outlet] = createUserWithOutlet(false);
$this->actingAs($user);
Livewire::test(Expense::class)
->set('form.description', 'Test Expense')
->set('form.amount', '50000')
->call('create')
->assertHasNoErrors(['form.outlet_id']);
});
it('can create an expense', function () {
[$user, $outlet] = createUserWithOutlet();
$this->actingAs($user);
Livewire::test(Expense::class)
->set('form.description', 'Buy Coffee')
->set('form.amount', '25000')
->call('create')
->assertDispatched('refreshDatatable');
$this->assertDatabaseHas('expenses', [
'description' => 'Buy Coffee',
'amount' => 25000,
'user_id' => $user->id,
'outlet_id' => $outlet->id,
]);
});
it('can create an expense with image', function () {
[$user, $outlet] = createUserWithOutlet();
$this->actingAs($user);
$image = UploadedFile::fake()->image('receipt.jpg');
// The WithMediaHandler contract seems to expect an array with 'path', not raw file object
$imageData = [['path' => $image->getPathname()]];
Livewire::test(Expense::class)
->set('form.description', 'Buy Coffee with Receipt')
->set('form.amount', '30000')
->set('form.image', $imageData)
->call('create');
$expense = ExpenseModel::where('description', 'Buy Coffee with Receipt')->first();
expect($expense)->not->toBeNull();
// Assuming media library is set up, verify media count or existence
// Note: In-memory/testing sqlite might behave differently with media library if not fully mocked,
// but basic relation check is good.
expect($expense->getMedia('image'))->toHaveCount(1);
});
it('can update an expense', function () {
[$user, $outlet] = createUserWithOutlet();
$this->actingAs($user);
$expense = ExpenseModel::factory()->create([
'user_id' => $user->id,
'outlet_id' => $outlet->id,
'description' => 'Old Description',
'amount' => 10000,
]);
Livewire::test(Expense::class)
->call('openModal', 'update', 'Edit Expense', $expense->hash)
->assertSet('form.description', 'Old Description')
->set('form.description', 'New Description')
->set('form.amount', '15000') // Updating amount
->call('update')
->assertDispatched('refreshDatatable');
$this->assertDatabaseHas('expenses', [
'id' => $expense->id,
'description' => 'New Description',
'amount' => 15000,
]);
});
it('can update an expense image', function () {
[$user, $outlet] = createUserWithOutlet();
$this->actingAs($user);
$expense = ExpenseModel::factory()->create([
'user_id' => $user->id,
'outlet_id' => $outlet->id,
'description' => 'Expense with Image',
'amount' => 10000,
]);
// Initial image upload (simulated via factory or direct DB check if needed,
// but here we just test REPLACING or ADDING via update)
$newImage = UploadedFile::fake()->image('new_receipt.jpg');
$imageData = [['path' => $newImage->getPathname()]];
Livewire::test(Expense::class)
->call('openModal', 'update', 'Edit Expense', $expense->hash)
->set('form.image', $imageData)
->call('update');
$expense->refresh();
expect($expense->getMedia('image'))->toHaveCount(1);
// In a real scenario we'd check filename, but hash names make it tricky in tests without mocking 'fake'
});
it('can delete an expense', function () {
[$user, $outlet] = createUserWithOutlet();
$this->actingAs($user);
$expense = ExpenseModel::factory()->create([
'user_id' => $user->id,
'outlet_id' => $outlet->id,
]);
Livewire::test(Expense::class)
->call('delete', $expense->hash)
->assertDispatched('refreshDatatable');
$this->assertSoftDeleted('expenses', [
'id' => $expense->id,
]);
});
// AUTHORIZATION TESTS
it('checks authorization for create', function () {
[$user, $outlet] = createUserWithOutlet();
$user->revokePermissionTo('create expense');
$this->actingAs($user);
Livewire::test(Expense::class)
->call('create')
->assertForbidden();
});
it('checks authorization for update', function () {
[$user, $outlet] = createUserWithOutlet();
$user->revokePermissionTo('update expense');
$this->actingAs($user);
$expense = ExpenseModel::factory()->create([
'user_id' => $user->id,
'outlet_id' => $outlet->id,
]);
Livewire::test(Expense::class)
->call('openModal', 'update', 'Edit', $expense->hash) // openModal doesn't check perm, update() does
->call('update')
->assertForbidden();
});
it('checks authorization for delete', function () {
[$user, $outlet] = createUserWithOutlet();
$user->revokePermissionTo('delete expense');
$this->actingAs($user);
$expense = ExpenseModel::factory()->create([
'user_id' => $user->id,
'outlet_id' => $outlet->id,
]);
Livewire::test(Expense::class)
->call('delete', $expense->hash)
->assertForbidden();
});

View File

@ -1,194 +0,0 @@
<?php
use App\Jobs\StorePushNotification;
use App\Livewire\Studio\Information\Broadcast;
use App\Models\Broadcast as BroadcastModel;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::class);
beforeEach(function () {
// Create permissions
Permission::create(['name' => 'create broadcast', 'guard_name' => 'web']);
Permission::create(['name' => 'update broadcast', 'guard_name' => 'web']);
Permission::create(['name' => 'delete broadcast', 'guard_name' => 'web']);
// Create role and assign permissions
$role = Role::create(['name' => 'Admin', 'guard_name' => 'web']);
$role->givePermissionTo(['create broadcast', 'update broadcast', 'delete broadcast']);
// Create admin user
$this->adminUser = User::factory()->create();
$this->adminUser->assignRole('Admin');
// Create additional roles for testing
Role::create(['name' => 'User', 'guard_name' => 'web']);
Role::create(['name' => 'Manager', 'guard_name' => 'web']);
});
it('renders page successfully', function () {
Livewire::actingAs($this->adminUser)
->test(Broadcast::class)
->assertOk()
->assertSet('method', 'create')
->assertSet('modalTitle', '')
->assertSee('Broadcast')
->assertSet('roles', function ($roles) {
return count($roles) > 0 && ! isset($roles['Developer']);
});
});
it('has title attribute', function () {
$component = new Broadcast;
$reflection = new \ReflectionClass($component);
$attributes = $reflection->getAttributes(\Livewire\Attributes\Title::class);
expect($attributes)->not->toBeEmpty();
});
it('excludes developer role on mount', function () {
Role::create(['name' => 'Developer', 'guard_name' => 'web']);
Livewire::actingAs($this->adminUser)
->test(Broadcast::class)
->assertSet('roles', function ($roles) {
return ! isset($roles['Developer']);
});
});
it('opens modal for create', function () {
Livewire::actingAs($this->adminUser)
->test(Broadcast::class)
->call('openModal', 'create', 'Tambah Broadcast')
->assertSet('method', 'create')
->assertSet('modalTitle', 'Tambah Broadcast')
->assertSet('form.title', '')
->assertSet('form.body', '')
->assertSet('form.role_ids', []);
});
it('opens modal for update', function () {
$broadcast = BroadcastModel::factory()->create([
'title' => 'Test Title',
'body' => 'Test Body',
]);
$role = Role::create(['name' => 'Test Role', 'guard_name' => 'web']);
$broadcast->audiences()->attach($role->id);
Livewire::actingAs($this->adminUser)
->test(Broadcast::class)
->call('openModal', 'update', 'Ubah Broadcast', $broadcast->hash)
->assertSet('method', 'update')
->assertSet('modalTitle', 'Ubah Broadcast');
});
it('creates broadcast successfully', function () {
Queue::fake();
$role1 = Role::where('name', 'User')->first();
$role2 = Role::where('name', 'Manager')->first();
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$user1->assignRole('User');
$user2->assignRole('Manager');
Livewire::actingAs($this->adminUser)
->test(Broadcast::class)
->set('form.title', 'Important Announcement')
->set('form.body', 'This is a test broadcast message.')
->set('form.role_ids', [$role1->id, $role2->id])
->call('create')
->assertDispatched('refreshDatatable');
$this->assertDatabaseHas('broadcasts', [
'user_id' => $this->adminUser->id,
'title' => 'Important Announcement',
'body' => 'This is a test broadcast message.',
]);
$broadcast = BroadcastModel::latest()->first();
expect($broadcast->audiences)->toHaveCount(2);
Queue::assertPushed(StorePushNotification::class);
});
it('requires permission to create broadcast', function () {
$userWithoutPermission = User::factory()->create();
Livewire::actingAs($userWithoutPermission)
->test(Broadcast::class)
->set('form.title', 'Test Title')
->set('form.body', 'Test Body')
->set('form.role_ids', [1])
->call('create')
->assertForbidden();
});
it('broadcasts cannot be updated', function () {
$broadcast = BroadcastModel::factory()->create();
// Broadcast component doesn't have update method, only create and delete
expect(method_exists(Broadcast::class, 'update'))->toBeFalse();
});
it('broadcasts do not support update permission check', function () {
// Broadcast component doesn't have update method, so no update permission test needed
expect(method_exists(Broadcast::class, 'update'))->toBeFalse();
});
it('deletes broadcast successfully', function () {
$broadcast = BroadcastModel::factory()->create();
Livewire::actingAs($this->adminUser)
->test(Broadcast::class)
->call('delete', $broadcast)
->assertDispatched('refreshDatatable');
$this->assertDatabaseMissing('broadcasts', [
'id' => $broadcast->id,
]);
});
it('displays validation errors', function () {
Livewire::actingAs($this->adminUser)
->test(Broadcast::class)
->set('form.title', '') // Empty title
->set('form.body', '') // Empty body
->set('form.role_ids', []) // Empty role_ids
->call('create')
->assertHasErrors(['form.title', 'form.body', 'form.role_ids']);
});
it('opens modal correctly', function () {
$broadcast = BroadcastModel::factory()->create();
Livewire::actingAs($this->adminUser)
->test(Broadcast::class)
->call('openModal', 'create', 'Tambah Broadcast')
->assertSet('method', 'create')
->assertSet('modalTitle', 'Tambah Broadcast');
});
it('creates broadcast with push notification job', function () {
Queue::fake();
$role = Role::where('name', 'User')->first();
$user = User::factory()->create();
$user->assignRole('User');
Livewire::actingAs($this->adminUser)
->test(Broadcast::class)
->set('form.title', 'Test Title')
->set('form.body', 'Test Body')
->set('form.role_ids', [$role->id])
->call('create');
Queue::assertPushed(StorePushNotification::class);
});

View File

@ -1,194 +0,0 @@
<?php
use App\Livewire\Studio\Information\Faq;
use App\Models\Faq as FaqModel;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::class);
beforeEach(function () {
// Create permissions
Permission::create(['name' => 'create faq', 'guard_name' => 'web']);
Permission::create(['name' => 'update faq', 'guard_name' => 'web']);
Permission::create(['name' => 'delete faq', 'guard_name' => 'web']);
// Create role and assign permissions
$role = Role::create(['name' => 'Admin', 'guard_name' => 'web']);
$role->givePermissionTo(['create faq', 'update faq', 'delete faq']);
// Create admin user
$this->adminUser = User::factory()->create();
$this->adminUser->assignRole('Admin');
});
it('renders page successfully', function () {
Livewire::actingAs($this->adminUser)
->test(Faq::class)
->assertOk()
->assertSet('method', 'create')
->assertSet('modalTitle', '')
->assertSee('FAQs');
});
it('has title attribute', function () {
$component = new Faq;
$reflection = new \ReflectionClass($component);
$attributes = $reflection->getAttributes(\Livewire\Attributes\Title::class);
expect($attributes)->not->toBeEmpty();
});
it('opens modal for create', function () {
Livewire::actingAs($this->adminUser)
->test(Faq::class)
->call('openModal', 'create', 'Tambah FAQs')
->assertSet('method', 'create')
->assertSet('modalTitle', 'Tambah FAQs')
->assertSet('form.question', '')
->assertSet('form.answer', '');
});
it('opens modal for update', function () {
$faq = FaqModel::factory()->create([
'question' => 'Test Question',
'answer' => 'Test Answer',
]);
Livewire::actingAs($this->adminUser)
->test(Faq::class)
->call('openModal', 'update', 'Ubah FAQs', $faq->hash)
->assertSet('method', 'update')
->assertSet('modalTitle', 'Ubah FAQs')
->assertSet('form.question', 'Test Question')
->assertSet('form.answer', 'Test Answer');
});
it('creates faq successfully', function () {
Livewire::actingAs($this->adminUser)
->test(Faq::class)
->set('form.question', 'What is Laravel?')
->set('form.answer', 'Laravel is a PHP framework.')
->call('create')
->assertDispatched('refreshDatatable');
$this->assertDatabaseHas('faqs', [
'question' => 'What is Laravel?',
'answer' => 'Laravel is a PHP framework.',
'sort_order' => 1,
]);
});
it('requires permission to create faq', function () {
$userWithoutPermission = User::factory()->create();
Livewire::actingAs($userWithoutPermission)
->test(Faq::class)
->set('form.question', 'Test Question')
->set('form.answer', 'Test Answer')
->call('create')
->assertForbidden();
});
it('updates faq successfully', function () {
$faq = FaqModel::factory()->create([
'question' => 'Old Question',
'answer' => 'Old Answer',
]);
Livewire::actingAs($this->adminUser)
->test(Faq::class)
->call('openModal', 'update', 'Ubah FAQs', $faq->hash)
->set('form.question', 'Updated Question')
->set('form.answer', 'Updated Answer')
->call('update')
->assertDispatched('refreshDatatable');
$this->assertDatabaseHas('faqs', [
'id' => $faq->id,
'question' => 'Updated Question',
'answer' => 'Updated Answer',
]);
});
it('requires permission to update faq', function () {
$faq = FaqModel::factory()->create();
$userWithoutPermission = User::factory()->create();
Livewire::actingAs($userWithoutPermission)
->test(Faq::class)
->call('openModal', 'update', 'Ubah FAQs', $faq->hash)
->set('form.question', 'Updated Question')
->set('form.answer', 'Updated Answer')
->call('update')
->assertForbidden();
});
it('deletes faq successfully', function () {
$faq = FaqModel::factory()->create();
Livewire::actingAs($this->adminUser)
->test(Faq::class)
->call('delete', $faq)
->assertDispatched('refreshDatatable');
expect($faq->trashed())->toBeTrue();
});
it('sorts order up', function () {
$faq1 = FaqModel::factory()->create(['sort_order' => 1]);
$faq2 = FaqModel::factory()->create(['sort_order' => 2]);
Livewire::actingAs($this->adminUser)
->test(Faq::class)
->call('sortOrder', $faq2, 'up')
->assertDispatched('refreshDatatable');
expect($faq1->fresh()->sort_order)->toBe(2);
expect($faq2->fresh()->sort_order)->toBe(1);
});
it('sorts order down', function () {
$faq1 = FaqModel::factory()->create(['sort_order' => 1]);
$faq2 = FaqModel::factory()->create(['sort_order' => 2]);
Livewire::actingAs($this->adminUser)
->test(Faq::class)
->call('sortOrder', $faq1, 'down')
->assertDispatched('refreshDatatable');
expect($faq1->fresh()->sort_order)->toBe(2);
expect($faq2->fresh()->sort_order)->toBe(1);
});
it('requires permission to sort order', function () {
$faq = FaqModel::factory()->create();
$userWithoutPermission = User::factory()->create();
Livewire::actingAs($userWithoutPermission)
->test(Faq::class)
->call('sortOrder', $faq, 'up')
->assertForbidden();
});
it('displays validation errors', function () {
Livewire::actingAs($this->adminUser)
->test(Faq::class)
->set('form.question', '') // Empty question
->set('form.answer', '') // Empty answer
->call('create')
->assertHasErrors(['form.question', 'form.answer']);
});
it('opens modal correctly', function () {
$faq = FaqModel::factory()->create();
Livewire::actingAs($this->adminUser)
->test(Faq::class)
->call('openModal', 'create', 'Tambah FAQs')
->assertSet('method', 'create')
->assertSet('modalTitle', 'Tambah FAQs');
});

View File

@ -1,643 +0,0 @@
<?php
use App\Enums\Gender;
use App\Livewire\Studio\Loyalty\Customer as CustomerComponent;
use App\Models\Customer;
use App\Models\Employee;
use App\Models\Order;
use App\Models\Outlet;
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\Role;
uses(RefreshDatabase::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 mountCustomerComponent(User $user)
{
return Livewire::actingAs($user)->test(CustomerComponent::class);
}
/*
|--------------------------------------------------------------------------
| Component Rendering & Mount
|--------------------------------------------------------------------------
*/
it('renders page successfully', function () {
mountCustomerComponent($this->user)
->assertViewIs('livewire.studio.loyalty.customers')
->assertViewHas('pageTitle', 'Customer');
});
it('initializes form properties on mount', function () {
$component = mountCustomerComponent($this->user);
expect($component->method)->toBe('create');
expect($component->modalTitle)->toBe('');
expect($component->form->name)->toBe('');
expect($component->form->phone_number)->toBeNull();
expect($component->form->gender)->toBe('');
});
it('loads stats on mount', function () {
// Create customers without users
Customer::factory()->count(3)->create(['user_id' => null]);
// Create customers with users
$user1 = User::factory()->create();
$user2 = User::factory()->create();
Customer::factory()->create(['user_id' => $user1->id]);
Customer::factory()->create(['user_id' => $user2->id]);
$component = mountCustomerComponent($this->user);
expect($component->stats)->toBeArray();
expect($component->stats)->toHaveCount(3);
expect($component->stats[0]['title'])->toBe('Total Customer');
expect($component->stats[1]['title'])->toBe('Total Member');
expect($component->stats[2]['title'])->toBe('Top Member');
});
it('calculates total customer count correctly', function () {
Customer::factory()->count(5)->create(['user_id' => null]);
Customer::factory()->count(3)->create(['user_id' => User::factory()->create()->id]);
$component = mountCustomerComponent($this->user);
expect($component->stats[0]['value'])->toBe(5);
});
it('calculates total member count correctly', function () {
Customer::factory()->count(2)->create(['user_id' => null]);
$user1 = User::factory()->create();
$user2 = User::factory()->create();
Customer::factory()->create(['user_id' => $user1->id]);
Customer::factory()->create(['user_id' => $user2->id]);
$component = mountCustomerComponent($this->user);
expect($component->stats[1]['value'])->toBe(2);
});
it('displays top member name correctly', function () {
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$customer1 = Customer::factory()->create(['user_id' => $user1->id, 'name' => 'Top Customer']);
$customer2 = Customer::factory()->create(['user_id' => $user2->id, 'name' => 'Regular Customer']);
// Create more orders for customer1
$outlet = Outlet::factory()->create();
Order::factory()->count(5)->create(['customer_id' => $customer1->id, 'user_id' => $user1->id, 'outlet_id' => $outlet->id]);
Order::factory()->count(2)->create(['customer_id' => $customer2->id, 'user_id' => $user2->id, 'outlet_id' => $outlet->id]);
$component = mountCustomerComponent($this->user);
expect($component->stats[2]['value'])->toBe('Top Customer');
});
it('displays null when no members exist', function () {
Customer::factory()->count(3)->create(['user_id' => null]);
$component = mountCustomerComponent($this->user);
expect($component->stats[2]['value'])->toBeNull();
});
it('displays null when members have no orders', function () {
$user = User::factory()->create();
Customer::factory()->create(['user_id' => $user->id]);
$component = mountCustomerComponent($this->user);
expect($component->stats[2]['value'])->toBeNull();
});
/*
|--------------------------------------------------------------------------
| Modal Management
|--------------------------------------------------------------------------
*/
it('opens modal with create method', function () {
$component = mountCustomerComponent($this->user)
->call('openModal', 'create', 'Tambah Customer');
expect($component->method)->toBe('create');
expect($component->modalTitle)->toBe('Tambah Customer');
});
it('opens modal with update method', function () {
$customer = Customer::factory()->create();
$component = mountCustomerComponent($this->user)
->call('openModal', 'update', 'Edit Customer', $customer->hash);
expect($component->method)->toBe('update');
expect($component->modalTitle)->toBe('Edit Customer');
});
it('sets form data when opening update modal', function () {
$customer = Customer::factory()->create([
'name' => 'John Doe',
'phone_number' => '0812 3456 7890',
'gender' => Gender::MALE->value,
]);
$component = mountCustomerComponent($this->user)
->call('openModal', 'update', 'Edit Customer', $customer->hash);
expect($component->form->name)->toBe('John Doe');
expect($component->form->phone_number)->toBe('0812 3456 7890');
expect($component->form->gender)->toEqual(Gender::MALE->value);
});
it('resets validation and error bag when opening modal', function () {
$component = mountCustomerComponent($this->user)
->call('openModal', 'create', 'Tambah Customer')
->set('form.name', '')
->call('create')
->assertHasErrors(['form.name']);
$component->call('openModal', 'create', 'Tambah Customer')
->assertHasNoErrors();
});
/*
|--------------------------------------------------------------------------
| Create Customer
|--------------------------------------------------------------------------
*/
it('prevents create when user is unauthorized', function () {
$this->ownerRole->revokePermissionTo('create customer');
Gate::define('create customer', fn () => false);
mountCustomerComponent($this->user)
->call('openModal', 'create', 'Tambah Customer')
->set('form.name', 'John Doe')
->set('form.gender', Gender::MALE->value)
->call('create')
->assertForbidden();
});
it('can create a customer when authorized', function () {
$data = [
'name' => 'John Doe',
'phone_number' => '0812 3456 7890',
'gender' => Gender::MALE->value,
];
mountCustomerComponent($this->user)
->call('openModal', 'create', 'Tambah Customer')
->set('form.name', $data['name'])
->set('form.phone_number', $data['phone_number'])
->set('form.gender', $data['gender'])
->call('create')
->assertHasNoErrors()
->assertDispatched('refreshDatatable');
$this->assertDatabaseHas('customers', [
'name' => $data['name'],
'phone_number' => $data['phone_number'],
'gender' => Gender::MALE->value,
]);
});
it('can create a customer without phone number', function () {
$data = [
'name' => 'Jane Doe',
'phone_number' => null,
'gender' => Gender::FEMALE->value,
];
mountCustomerComponent($this->user)
->call('openModal', 'create', 'Tambah Customer')
->set('form.name', $data['name'])
->set('form.phone_number', $data['phone_number'])
->set('form.gender', $data['gender'])
->call('create')
->assertHasNoErrors();
$this->assertDatabaseHas('customers', [
'name' => $data['name'],
'phone_number' => null,
'gender' => Gender::FEMALE->value,
]);
});
it('can create a customer with user_id', function () {
$user = User::factory()->create();
$data = [
'name' => 'Member Customer',
'gender' => Gender::MALE->value,
];
mountCustomerComponent($this->user)
->call('openModal', 'create', 'Tambah Customer')
->set('form.name', $data['name'])
->set('form.gender', $data['gender'])
->set('form.user_id', $user->id)
->call('create')
->assertHasNoErrors();
$this->assertDatabaseHas('customers', [
'name' => $data['name'],
'user_id' => $user->id,
]);
});
it('validates required fields when creating customer', function () {
mountCustomerComponent($this->user)
->call('openModal', 'create', 'Tambah Customer')
->call('create')
->assertHasErrors([
'form.name',
'form.gender',
]);
});
it('validates name field max length when creating customer', function () {
mountCustomerComponent($this->user)
->call('openModal', 'create', 'Tambah Customer')
->set('form.name', str_repeat('a', 51))
->set('form.gender', Gender::MALE->value)
->call('create')
->assertHasErrors(['form.name']);
});
it('validates gender field is required when creating customer', function () {
mountCustomerComponent($this->user)
->call('openModal', 'create', 'Tambah Customer')
->set('form.name', 'John Doe')
->set('form.gender', '')
->call('create')
->assertHasErrors(['form.gender']);
});
it('validates gender field is valid enum when creating customer', function () {
mountCustomerComponent($this->user)
->call('openModal', 'create', 'Tambah Customer')
->set('form.name', 'John Doe')
->set('form.gender', 'invalid')
->call('create')
->assertHasErrors(['form.gender']);
});
it('validates phone number format when creating customer', function () {
mountCustomerComponent($this->user)
->call('openModal', 'create', 'Tambah Customer')
->set('form.name', 'John Doe')
->set('form.phone_number', 'invalid-phone')
->set('form.gender', Gender::MALE->value)
->call('create')
->assertHasErrors(['form.phone_number']);
});
it('accepts valid phone number format when creating customer', function () {
mountCustomerComponent($this->user)
->call('openModal', 'create', 'Tambah Customer')
->set('form.name', 'John Doe')
->set('form.phone_number', '0812 3456 7890')
->set('form.gender', Gender::MALE->value)
->call('create')
->assertHasNoErrors();
});
it('dispatches refreshDatatable event after creating customer', function () {
mountCustomerComponent($this->user)
->call('openModal', 'create', 'Tambah Customer')
->set('form.name', 'John Doe')
->set('form.gender', Gender::MALE->value)
->call('create')
->assertDispatched('refreshDatatable');
});
/*
|--------------------------------------------------------------------------
| Update Customer
|--------------------------------------------------------------------------
*/
it('prevents update when user is unauthorized', function () {
$this->ownerRole->revokePermissionTo('update customer');
Gate::define('update customer', fn () => false);
$customer = Customer::factory()->create();
mountCustomerComponent($this->user)
->call('openModal', 'update', 'Edit Customer', $customer->hash)
->set('form.name', 'Updated Name')
->call('update')
->assertForbidden();
});
it('can update a customer when authorized', function () {
$customer = Customer::factory()->create([
'name' => 'John Doe',
'phone_number' => '0812 3456 7890',
'gender' => Gender::MALE,
]);
mountCustomerComponent($this->user)
->call('openModal', 'update', 'Edit Customer', $customer->hash)
->assertSet('form.name', 'John Doe')
->assertSet('form.phone_number', '0812 3456 7890')
->assertSet('form.gender', Gender::MALE->value)
->set('form.name', 'Jane Doe')
->set('form.phone_number', '0812 9999 8888')
->set('form.gender', Gender::FEMALE->value)
->call('update')
->assertHasNoErrors()
->assertDispatched('refreshDatatable');
$customer->refresh();
expect($customer->name)->toBe('Jane Doe');
expect($customer->phone_number)->toBe('0812 9999 8888');
expect($customer->gender)->toBe(Gender::FEMALE);
});
it('can update customer phone number to null', function () {
$customer = Customer::factory()->create([
'phone_number' => '0812 3456 7890',
]);
mountCustomerComponent($this->user)
->call('openModal', 'update', 'Edit Customer', $customer->hash)
->set('form.phone_number', null)
->call('update')
->assertHasNoErrors();
$customer->refresh();
expect($customer->phone_number)->toBeNull();
});
it('validates required fields when updating customer', function () {
$customer = Customer::factory()->create();
mountCustomerComponent($this->user)
->call('openModal', 'update', 'Edit Customer', $customer->hash)
->set('form.name', '')
->set('form.gender', '')
->call('update')
->assertHasErrors([
'form.name',
'form.gender',
]);
});
it('validates name field max length when updating customer', function () {
$customer = Customer::factory()->create();
mountCustomerComponent($this->user)
->call('openModal', 'update', 'Edit Customer', $customer->hash)
->set('form.name', str_repeat('a', 51))
->call('update')
->assertHasErrors(['form.name']);
});
it('validates phone number format when updating customer', function () {
$customer = Customer::factory()->create();
mountCustomerComponent($this->user)
->call('openModal', 'update', 'Edit Customer', $customer->hash)
->set('form.phone_number', 'invalid-phone')
->call('update')
->assertHasErrors(['form.phone_number']);
});
it('dispatches refreshDatatable event after updating customer', function () {
$customer = Customer::factory()->create();
mountCustomerComponent($this->user)
->call('openModal', 'update', 'Edit Customer', $customer->hash)
->set('form.name', 'Updated Name')
->call('update')
->assertDispatched('refreshDatatable');
});
/*
|--------------------------------------------------------------------------
| Delete Customer
|--------------------------------------------------------------------------
*/
it('can delete a customer', function () {
$customer = Customer::factory()->create();
mountCustomerComponent($this->user)
->call('delete', $customer)
->assertHasNoErrors()
->assertDispatched('refreshDatatable');
expect(Customer::count())->toBe(0);
$this->assertSoftDeleted('customers', ['id' => $customer->id]);
});
it('soft deletes customer when deleting', function () {
$customer = Customer::factory()->create();
mountCustomerComponent($this->user)
->call('delete', $customer);
$customer->refresh();
expect($customer->trashed())->toBeTrue();
});
it('dispatches refreshDatatable event after deleting customer', function () {
$customer = Customer::factory()->create();
mountCustomerComponent($this->user)
->call('delete', $customer)
->assertDispatched('refreshDatatable');
});
it('can delete customer with orders', function () {
$outlet = Outlet::factory()->create();
$user = User::factory()->create();
$customer = Customer::factory()->create(['user_id' => $user->id]);
Order::factory()->count(3)->create([
'customer_id' => $customer->id,
'user_id' => $user->id,
'outlet_id' => $outlet->id,
]);
mountCustomerComponent($this->user)
->call('delete', $customer);
expect($customer->trashed())->toBeTrue();
// Orders should still exist (cascade delete might be configured differently)
expect(Order::count())->toBe(3);
});
/*
|--------------------------------------------------------------------------
| Stats Recalculation
|--------------------------------------------------------------------------
*/
it('recalculates stats after creating customer', function () {
$initialStats = mountCustomerComponent($this->user)->stats;
$initialCustomerCount = $initialStats[0]['value'];
mountCustomerComponent($this->user)
->call('openModal', 'create', 'Tambah Customer')
->set('form.name', 'New Customer')
->set('form.gender', Gender::MALE->value)
->call('create');
$newComponent = mountCustomerComponent($this->user);
expect($newComponent->stats[0]['value'])->toBe($initialCustomerCount + 1);
});
it('recalculates stats after creating member customer', function () {
$user = User::factory()->create();
mountCustomerComponent($this->user)
->call('openModal', 'create', 'Tambah Customer')
->set('form.name', 'New Member')
->set('form.gender', Gender::MALE->value)
->set('form.user_id', $user->id)
->call('create');
$newComponent = mountCustomerComponent($this->user);
expect($newComponent->stats[1]['value'])->toBeGreaterThan(0);
});
it('recalculates top member after creating orders', function () {
$user = User::factory()->create();
$customer = Customer::factory()->create(['user_id' => $user->id, 'name' => 'Top Member']);
$outlet = Outlet::factory()->create();
Order::factory()->count(5)->create([
'customer_id' => $customer->id,
'user_id' => $user->id,
'outlet_id' => $outlet->id,
]);
$component = mountCustomerComponent($this->user);
expect($component->stats[2]['value'])->toBe('Top Member');
});
/*
|--------------------------------------------------------------------------
| Edge Cases
|--------------------------------------------------------------------------
*/
it('handles customer with very long name at max length', function () {
$longName = str_repeat('a', 50);
mountCustomerComponent($this->user)
->call('openModal', 'create', 'Tambah Customer')
->set('form.name', $longName)
->set('form.gender', Gender::MALE->value)
->call('create')
->assertHasNoErrors();
$this->assertDatabaseHas('customers', ['name' => $longName]);
});
it('handles customer with all gender types', function () {
foreach (Gender::cases() as $gender) {
mountCustomerComponent($this->user)
->call('openModal', 'create', 'Tambah Customer')
->set('form.name', "Customer {$gender->name}")
->set('form.gender', $gender->value)
->call('create')
->assertHasNoErrors();
}
expect(Customer::where('gender', Gender::MALE->value)->count())->toBeGreaterThan(0);
expect(Customer::where('gender', Gender::FEMALE->value)->count())->toBeGreaterThan(0);
});
it('handles updating customer with same data', function () {
$customer = Customer::factory()->create([
'name' => 'John Doe',
'phone_number' => '0812 3456 7890',
'gender' => Gender::MALE,
]);
mountCustomerComponent($this->user)
->call('openModal', 'update', 'Edit Customer', $customer->hash)
->call('update')
->assertHasNoErrors();
$customer->refresh();
expect($customer->name)->toBe('John Doe');
});
it('handles customer with special characters in name', function () {
$specialName = "O'Brien & Co. - Test";
mountCustomerComponent($this->user)
->call('openModal', 'create', 'Tambah Customer')
->set('form.name', $specialName)
->set('form.gender', Gender::MALE->value)
->call('create')
->assertHasNoErrors();
$this->assertDatabaseHas('customers', ['name' => $specialName]);
});
it('handles multiple customers with same phone number', function () {
$phoneNumber = '0812 3456 7890';
mountCustomerComponent($this->user)
->call('openModal', 'create', 'Tambah Customer')
->set('form.name', 'Customer 1')
->set('form.phone_number', $phoneNumber)
->set('form.gender', Gender::MALE->value)
->call('create')
->assertHasNoErrors();
mountCustomerComponent($this->user)
->call('openModal', 'create', 'Tambah Customer')
->set('form.name', 'Customer 2')
->set('form.phone_number', $phoneNumber)
->set('form.gender', Gender::FEMALE->value)
->call('create')
->assertHasNoErrors();
expect(Customer::where('phone_number', $phoneNumber)->count())->toBe(2);
});
it('handles empty stats when no customers exist', function () {
$component = mountCustomerComponent($this->user);
expect($component->stats[0]['value'])->toBe(0);
expect($component->stats[1]['value'])->toBe(0);
expect($component->stats[2]['value'])->toBeNull();
});
it('handles customer deletion when customer does not exist', function () {
$customer = Customer::factory()->create();
$customerId = $customer->id;
$customer->delete();
// Try to delete already deleted customer
$deletedCustomer = Customer::withTrashed()->find($customerId);
mountCustomerComponent($this->user)
->call('delete', $deletedCustomer)
->assertHasNoErrors();
expect(Customer::withTrashed()->find($customerId)->trashed())->toBeTrue();
});

View File

@ -1,649 +0,0 @@
<?php
use App\Livewire\Studio\Loyalty\Tier;
use App\Models\Bottle;
use App\Models\Brand;
use App\Models\Customer;
use App\Models\Employee;
use App\Models\Membership;
use App\Models\Order;
use App\Models\OrderItem;
use App\Models\Outlet;
use App\Models\Perfume;
use App\Models\Product;
use App\Models\Tier as TierModel;
use App\Models\TierReward;
use App\Models\User;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
use Livewire\Livewire;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::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 mountTierComponent(User $user)
{
return Livewire::actingAs($user)->test(Tier::class);
}
/*
|--------------------------------------------------------------------------
| Component Rendering & Mount
|--------------------------------------------------------------------------
*/
it('renders page successfully', function () {
mountTierComponent($this->user)
->assertViewIs('livewire.studio.loyalty.tiers')
->assertViewHas('pageTitle', 'Tier');
});
it('loads tiers on mount', function () {
$tier1 = TierModel::factory()->create(['name' => 'Bronze', 'min_spending' => 0, 'max_spending' => 100000]);
$tier2 = TierModel::factory()->create(['name' => 'Silver', 'min_spending' => 100001, 'max_spending' => 500000]);
$component = mountTierComponent($this->user);
expect($component->tiers)->toBeInstanceOf(\Illuminate\Support\Collection::class);
expect($component->tiers)->toHaveCount(2);
expect($component->tiers[0]->name)->toBe('Bronze');
expect($component->tiers[1]->name)->toBe('Silver');
});
it('displays all tiers in the view', function () {
$tier = TierModel::factory()->create(['name' => 'Gold', 'min_spending' => 1000000, 'max_spending' => 5000000]);
mountTierComponent($this->user)
->assertSee('Gold')
->assertSee('1.000.000');
});
it('sorts tiers by min_spending ascending', function () {
$tier3 = TierModel::factory()->create(['name' => 'Platinum', 'min_spending' => 10000000, 'max_spending' => null]);
$tier1 = TierModel::factory()->create(['name' => 'Bronze', 'min_spending' => 0, 'max_spending' => 1000000]);
$tier2 = TierModel::factory()->create(['name' => 'Silver', 'min_spending' => 1000001, 'max_spending' => 5000000]);
$component = mountTierComponent($this->user);
expect($component->tiers)->toBeInstanceOf(\Illuminate\Support\Collection::class);
expect($component->tiers[0]->name)->toBe('Bronze');
expect($component->tiers[1]->name)->toBe('Silver');
expect($component->tiers[2]->name)->toBe('Platinum');
});
/*
|--------------------------------------------------------------------------
| Create Tier
|--------------------------------------------------------------------------
*/
it('prevents create when user is unauthorized', function () {
$this->ownerRole->revokePermissionTo('create tier');
Gate::define('create tier', fn () => false);
mountTierComponent($this->user)
->set('form.name', 'Gold')
->set('form.min_spending', '1000000')
->set('form.max_spending', '5000000')
->call('create')
->assertForbidden();
});
it('can create a tier when authorized', function () {
mountTierComponent($this->user)
->call('openModal', 'create', 'Tambah Tier')
->set('form.name', 'Gold')
->set('form.min_spending', '1.000.000')
->set('form.max_spending', '5.000.000')
->call('create')
->assertHasNoErrors()
->assertSet('tiers', function ($tiers) {
return $tiers instanceof \Illuminate\Support\Collection &&
$tiers->count() === 1 &&
$tiers[0]->name === 'Gold';
});
$this->assertDatabaseHas('tiers', [
'name' => 'Gold',
'min_spending' => 1000000,
'max_spending' => null, // reorderTierChain makes last tier null
]);
});
it('can create a tier without max_spending', function () {
mountTierComponent($this->user)
->call('openModal', 'create', 'Tambah Tier')
->set('form.name', 'Platinum')
->set('form.min_spending', '5.000.001')
->set('form.max_spending', null)
->call('create')
->assertHasNoErrors();
$this->assertDatabaseHas('tiers', [
'name' => 'Platinum',
'min_spending' => 5000001,
'max_spending' => null,
]);
});
it('sorts tiers after creating new tier', function () {
TierModel::factory()->create(['name' => 'Silver', 'min_spending' => 500001, 'max_spending' => 1000000]);
TierModel::factory()->create(['name' => 'Bronze', 'min_spending' => 0, 'max_spending' => 500000]);
$component = mountTierComponent($this->user)
->call('openModal', 'create', 'Tambah Tier')
->set('form.name', 'Gold')
->set('form.min_spending', '1.000.001')
->set('form.max_spending', '2.000.000')
->call('create');
expect($component->tiers)->toBeInstanceOf(\Illuminate\Support\Collection::class);
expect($component->tiers[0]->name)->toBe('Bronze');
expect($component->tiers[1]->name)->toBe('Silver');
expect($component->tiers[2]->name)->toBe('Gold');
});
it('validates required fields when creating tier', function () {
mountTierComponent($this->user)
->call('openModal', 'create', 'Tambah Tier')
->call('create')
->assertHasErrors([
'form.name',
'form.min_spending',
]);
});
it('validates name field max length when creating tier', function () {
mountTierComponent($this->user)
->call('openModal', 'create', 'Tambah Tier')
->set('form.name', str_repeat('a', 51))
->set('form.min_spending', '100')
->call('create')
->assertHasErrors(['form.name']);
});
it('validates min_spending is unsigned integer when creating tier', function () {
mountTierComponent($this->user)
->call('openModal', 'create', 'Tambah Tier')
->set('form.name', 'Gold')
->set('form.min_spending', '-100')
->call('create')
->assertHasErrors(['form.min_spending']);
});
it('validates max_spending is unsigned integer when creating tier', function () {
mountTierComponent($this->user)
->call('openModal', 'create', 'Tambah Tier')
->set('form.name', 'Gold')
->set('form.min_spending', '100')
->set('form.max_spending', '-50')
->call('create')
->assertHasErrors(['form.max_spending']);
});
/*
|--------------------------------------------------------------------------
| Update Tier
|--------------------------------------------------------------------------
*/
it('prevents update when user is unauthorized', function () {
$this->ownerRole->revokePermissionTo('update tier');
Gate::define('update tier', fn () => false);
$tier = TierModel::factory()->create(['name' => 'Bronze', 'min_spending' => 0, 'max_spending' => 100000]);
mountTierComponent($this->user)
->call('openModal', 'update', 'Ubah Tier', $tier->hash)
->set('form.name', 'Gold')
->call('update')
->assertForbidden();
});
it('can update a tier when authorized', function () {
$tier = TierModel::factory()->create(['name' => 'Bronze', 'min_spending' => 0, 'max_spending' => 100000]);
mountTierComponent($this->user)
->call('openModal', 'update', 'Ubah Tier', $tier->hash)
->assertSet('form.name', 'Bronze')
->assertSet('form.min_spending', formatCurrencyNumber($tier->min_spending))
->assertSet('form.max_spending', formatCurrencyNumber($tier->max_spending))
->set('form.name', 'Gold')
->set('form.min_spending', '1.000.000')
->set('form.max_spending', '5.000.000')
->call('update')
->assertHasNoErrors()
->assertSet('tiers', function ($tiers) use ($tier) {
$updatedTier = $tiers->firstWhere('hash', $tier->hash);
return $updatedTier->name === 'Gold';
});
$tier->refresh();
expect($tier->name)->toBe('Gold');
expect($tier->min_spending)->toBe(1000000);
expect($tier->max_spending)->toBeNull(); // reorderTierChain makes last tier null
});
it('can update tier without max_spending', function () {
$tier = TierModel::factory()->create(['name' => 'Bronze', 'min_spending' => 0, 'max_spending' => 100000]);
mountTierComponent($this->user)
->call('openModal', 'update', 'Ubah Tier', $tier->hash)
->set('form.max_spending', null)
->call('update')
->assertHasNoErrors();
$tier->refresh();
expect($tier->max_spending)->toBeNull();
});
it('validates required fields when updating tier', function () {
$tier = TierModel::factory()->create();
mountTierComponent($this->user)
->call('openModal', 'update', 'Ubah Tier', $tier->hash)
->set('form.name', '')
->set('form.min_spending', '')
->call('update')
->assertHasErrors([
'form.name',
'form.min_spending',
]);
});
it('validates name field max length when updating tier', function () {
$tier = TierModel::factory()->create();
mountTierComponent($this->user)
->call('openModal', 'update', 'Ubah Tier', $tier->hash)
->set('form.name', str_repeat('a', 51))
->call('update')
->assertHasErrors(['form.name']);
});
it('resets validation and error bag when opening modal', function () {
$tier = TierModel::factory()->create();
$component = mountTierComponent($this->user)
->call('openModal', 'update', 'Ubah Tier', $tier->hash)
->set('form.name', '')
->call('update')
->assertHasErrors(['form.name']);
$component->call('openModal', 'update', 'Ubah Tier', $tier->hash)
->assertHasNoErrors();
});
/*
|--------------------------------------------------------------------------
| Delete Tier
|--------------------------------------------------------------------------
*/
it('can delete a tier when authorized', function () {
$tier = TierModel::factory()->create(['name' => 'Bronze']);
mountTierComponent($this->user)
->call('delete', $tier)
->assertHasNoErrors()
->assertSet('tiers', function ($tiers) use ($tier) {
return ! $tiers->contains(function ($t) use ($tier) {
return $t->hash === $tier->hash;
});
});
expect(TierModel::count())->toBe(0);
$this->assertSoftDeleted('tiers', ['id' => $tier->id]);
});
it('cascades delete to memberships when deleting tier', function () {
$tier = TierModel::factory()->create();
$user = User::factory()->create();
$customer = Customer::factory()->create(['user_id' => $user->id]);
$membership = Membership::create([
'user_id' => $user->id,
'tier_id' => $tier->id,
'total_spending' => 100000,
]);
mountTierComponent($this->user)
->call('delete', $tier)
->assertHasNoErrors();
expect(Membership::count())->toBe(0);
$this->assertSoftDeleted('memberships', ['id' => $membership->id]);
});
/*
|--------------------------------------------------------------------------
| Load Tiers
|--------------------------------------------------------------------------
*/
it('loads tiers with membership count', function () {
$tier = TierModel::factory()->create();
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$customer1 = Customer::factory()->create(['user_id' => $user1->id]);
$customer2 = Customer::factory()->create(['user_id' => $user2->id]);
Membership::create(['user_id' => $user1->id, 'tier_id' => $tier->id, 'total_spending' => 100000]);
Membership::create(['user_id' => $user2->id, 'tier_id' => $tier->id, 'total_spending' => 200000]);
$component = mountTierComponent($this->user);
$loadedTier = $component->tiers->firstWhere('id', $tier->id);
expect($loadedTier->total_members)->toBe('2');
});
it('loads tiers with rewards count', function () {
$tier = TierModel::factory()->create();
TierReward::create(['tier_id' => $tier->id, 'name' => 'Reward 1', 'points' => 100]);
TierReward::create(['tier_id' => $tier->id, 'name' => 'Reward 2', 'points' => 200]);
TierReward::create(['tier_id' => $tier->id, 'name' => 'Reward 3', 'points' => 300]);
$component = mountTierComponent($this->user);
$loadedTier = $component->tiers->firstWhere('id', $tier->id);
expect($loadedTier['total_rewards'])->toBe('3');
});
it('loads top member stats when tier has memberships', function () {
$outlet = Outlet::factory()->operational()->create();
$tier = TierModel::factory()->create(['name' => 'Gold']);
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$customer1 = Customer::factory()->create(['user_id' => $user1->id, 'name' => 'John Doe']);
$customer2 = Customer::factory()->create(['user_id' => $user2->id, 'name' => 'Jane Doe']);
// Create memberships with different total_spending
$membership1 = Membership::create([
'user_id' => $user1->id,
'tier_id' => $tier->id,
'total_spending' => 5000000, // Higher spending
]);
$membership2 = Membership::create([
'user_id' => $user2->id,
'tier_id' => $tier->id,
'total_spending' => 2000000, // Lower spending
]);
// Create orders for user1 (top member)
$order1 = Order::create([
'outlet_id' => $outlet->id,
'customer_id' => $customer1->id,
'user_id' => $user1->id,
'invoice_number' => 'INV-001',
'cogs' => 50000,
'subtotal' => 100000,
'discount' => 10000,
'total' => 90000,
'ordered_at' => now(),
]);
$order2 = Order::create([
'outlet_id' => $outlet->id,
'customer_id' => $customer1->id,
'user_id' => $user1->id,
'invoice_number' => 'INV-002',
'cogs' => 25000,
'subtotal' => 50000,
'discount' => 5000,
'total' => 45000,
'ordered_at' => now(),
]);
// Create order items for order1
$brand = Brand::factory()->create();
$perfume1 = Perfume::create([
'brand_id' => $brand->id,
'name' => 'Test Perfume 1',
'slug' => 'test-perfume-1',
'sku' => 'TP1-0001',
'cost_price' => 10000,
'sale_price' => 50000,
]);
$product1 = Product::create([
'name' => 'Test Product 1',
'slug' => 'test-product-1',
'sku' => 'PR1-0001',
'cost_price' => 5000,
'sale_price' => 25000,
]);
$bottle1 = Bottle::factory()->create();
OrderItem::create([
'order_id' => $order1->id,
'user_id' => $user1->id,
'orderable_type' => Perfume::class,
'orderable_id' => $perfume1->id,
'cogs' => 10000,
'unit_price' => 50000,
'quantity' => 2,
]);
OrderItem::create([
'order_id' => $order1->id,
'user_id' => $user1->id,
'orderable_type' => Product::class,
'orderable_id' => $product1->id,
'cogs' => 5000,
'unit_price' => 25000,
'quantity' => 3,
]);
OrderItem::create([
'order_id' => $order1->id,
'user_id' => $user1->id,
'orderable_type' => Bottle::class,
'orderable_id' => $bottle1->id,
'cogs' => 3000,
'unit_price' => 15000,
'quantity' => 1,
]);
// Create order items for order2
$perfume2 = Perfume::create([
'brand_id' => $brand->id,
'name' => 'Test Perfume 2',
'slug' => 'test-perfume-2',
'sku' => 'TP2-0001',
'cost_price' => 10000,
'sale_price' => 50000,
]);
OrderItem::create([
'order_id' => $order2->id,
'user_id' => $user1->id,
'orderable_type' => Perfume::class,
'orderable_id' => $perfume2->id,
'cogs' => 10000,
'unit_price' => 50000,
'quantity' => 1,
]);
$component = mountTierComponent($this->user);
$loadedTier = $component->tiers->firstWhere('id', $tier->id);
expect($loadedTier->top_member)->not->toBeNull();
expect($loadedTier->top_member['name'])->toBe('John Doe');
expect($loadedTier->top_member['spending'])->toContain('5.000.000');
expect($loadedTier->top_member['total_orders'])->toBe('2');
expect($loadedTier->top_member['items']['total_perfume'])->toBe('3');
expect($loadedTier->top_member['items']['total_product'])->toContain('3');
expect($loadedTier->top_member['items']['total_bottle'])->toContain('1');
});
it('shows null top member when tier has no memberships', function () {
$tier = TierModel::factory()->create();
$component = mountTierComponent($this->user);
$loadedTier = $component->tiers->firstWhere('id', $tier->id);
expect($loadedTier->top_member)->toBeNull();
});
it('reloads tiers when data:tiersUpdated event is dispatched', function () {
$tier1 = TierModel::factory()->create(['name' => 'Bronze']);
$component = mountTierComponent($this->user);
expect($component->tiers)->toHaveCount(1);
$tier2 = TierModel::factory()->create(['name' => 'Silver']);
$component->dispatch('data:tiersUpdated')
->assertSet('tiers', function ($tiers) {
return $tiers->count() === 2;
});
});
/*
|--------------------------------------------------------------------------
| Modal Management
|--------------------------------------------------------------------------
*/
it('opens modal with correct method and title for create', function () {
$component = mountTierComponent($this->user)
->call('openModal', 'create', 'Tambah Tier');
expect($component->method)->toBe('create');
expect($component->modalTitle)->toBe('Tambah Tier');
});
it('opens modal with correct method and title for update', function () {
$tier = TierModel::factory()->create();
$component = mountTierComponent($this->user)
->call('openModal', 'update', 'Ubah Tier', $tier->hash);
expect($component->method)->toBe('update');
expect($component->modalTitle)->toBe('Ubah Tier');
expect($component->form->name)->toBe($tier->name);
expect($component->form->min_spending)->toBe(formatCurrencyNumber($tier->min_spending));
expect($component->form->max_spending)->toBe($tier->max_spending ? formatCurrencyNumber($tier->max_spending) : null);
});
it('sets form data when opening update modal', function () {
$tier = TierModel::factory()->create([
'name' => 'Gold',
'min_spending' => 1000000,
'max_spending' => 5000000,
]);
$component = mountTierComponent($this->user)
->call('openModal', 'update', 'Ubah Tier', $tier->hash);
expect($component->form->name)->toBe('Gold');
expect($component->form->min_spending)->toBe('1.000.000');
expect($component->form->max_spending)->toBe('5.000.000');
});
/*
|--------------------------------------------------------------------------
| Form Data Formatting
|--------------------------------------------------------------------------
*/
it('formats spending with currency when displaying', function () {
$tier = TierModel::factory()->create([
'min_spending' => 1000000,
'max_spending' => 5000000,
]);
$component = mountTierComponent($this->user);
$loadedTier = $component->tiers->firstWhere('id', $tier->id);
expect(formatCurrencyNumber($loadedTier->min_spending))->toContain('1.000.000');
expect(formatCurrencyNumber($loadedTier->max_spending))->toContain('5.000.000');
});
it('handles currency formatting in form input', function () {
mountTierComponent($this->user)
->call('openModal', 'create', 'Tambah Tier')
->set('form.name', 'Gold')
->set('form.min_spending', '1.000.000')
->set('form.max_spending', '5.000.000')
->call('create')
->assertHasNoErrors();
$tier = TierModel::first();
expect($tier->min_spending)->toBe(1000000);
expect($tier->max_spending)->toBeNull(); // ReorderTierChain makes the last tier null
});
/*
|--------------------------------------------------------------------------
| Edge Cases
|--------------------------------------------------------------------------
*/
it('handles tier with null max_spending correctly', function () {
$tier = TierModel::factory()->create([
'name' => 'Platinum',
'min_spending' => 10000000,
'max_spending' => null,
]);
$component = mountTierComponent($this->user);
$loadedTier = $component->tiers->firstWhere('id', $tier->id);
expect(formatCurrencyNumber($loadedTier->max_spending))->not->toBeNull();
});
it('handles tier with zero min_spending', function () {
$tier = TierModel::factory()->create([
'name' => 'Starter',
'min_spending' => 0,
'max_spending' => 100000,
]);
$component = mountTierComponent($this->user);
$loadedTier = $component->tiers->firstWhere('id', $tier->id);
expect(formatCurrencyNumber($loadedTier->min_spending))->toContain('0');
});
it('handles empty tiers list', function () {
$component = mountTierComponent($this->user);
expect($component->tiers)->toBeInstanceOf(Collection::class);
});
it('updates tier array correctly after update', function () {
$tier = TierModel::factory()->create([
'name' => 'Bronze',
'min_spending' => 0,
'max_spending' => 100000,
]);
$component = mountTierComponent($this->user)
->call('openModal', 'update', 'Ubah Tier', $tier->hash)
->set('form.name', 'Gold')
->set('form.min_spending', '1.000.000')
->set('form.max_spending', '5.000.000')
->call('update');
$updatedTier = $component->tiers->firstWhere('hash', $tier->hash);
expect($updatedTier->name)->toBe('Gold');
expect($updatedTier->formatted_min_spending)->toContain('1.000.000');
expect($updatedTier->max_spending)->toBeNull(); // ReorderTierChain makes the last tier null
});

View File

@ -1,596 +0,0 @@
<?php
use App\Enums\VoucherType;
use App\Jobs\StorePushNotification;
use App\Livewire\Studio\Loyalty\Voucher\Create;
use App\Models\Employee;
use App\Models\Facility;
use App\Models\OpeningHour;
use App\Models\Outlet;
use App\Models\PushNotification;
use App\Models\User;
use App\Models\Voucher;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Gate;
use Livewire\Livewire;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::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);
Bus::fake();
});
function mountCreateComponent(User $user)
{
return Livewire::actingAs($user)->test(Create::class);
}
function createOutlets(int $count = 3)
{
return Outlet::factory()
->count($count)
->has(OpeningHour::factory()->count(7))
->has(Facility::factory()->count(5))
->create();
}
/*
|--------------------------------------------------------------------------
| Component Rendering & Mount
|--------------------------------------------------------------------------
*/
it('renders page successfully', function () {
mountCreateComponent($this->user)
->assertViewIs('livewire.studio.loyalty.voucher.form')
->assertViewHas('pageTitle', 'Tambah Voucher');
});
it('loads outlets on mount', function () {
$outlets = createOutlets(3);
$component = mountCreateComponent($this->user);
expect($component->outlets)->toHaveCount(3);
expect($component->outlets)->toHaveKey($outlets->first()->id);
expect($component->outlets[$outlets->first()->id])->toBe($outlets->first()->name);
});
it('initializes form with default values', function () {
$component = mountCreateComponent($this->user);
expect($component->form->name)->toBe('');
expect($component->form->code)->toBe('');
expect($component->form->tags)->toBe('');
expect($component->form->type)->toBe('1');
expect($component->form->discount_amount)->toBe('');
expect($component->form->outlet_ids)->toBe([]);
});
/*
|--------------------------------------------------------------------------
| Create Voucher - Fixed Type
|--------------------------------------------------------------------------
*/
it('can create a fixed type voucher with all fields', function () {
$outlets = createOutlets(3);
$outletIds = $outlets->pluck('id')->toArray();
$component = mountCreateComponent($this->user)
->set('form.name', 'Voucher Diskon 10rb')
->set('form.code', 'DISKON10')
->set('form.tags', 'Member Baru')
->set('form.type', VoucherType::FIXED->value)
->set('form.discount_amount', '10000')
->set('form.min_purchase', '50000')
->set('form.quota', '100')
->set('form.limit_per_user', '2')
->set('form.summary', 'Diskon 10rb untuk pembelian minimal 50rb')
->set('form.start_date', now()->addDay()->format('Y-m-d'))
->set('form.end_date', now()->addMonth()->format('Y-m-d'))
->set('form.outlet_ids', $outletIds)
->call('save');
$component->assertHasNoErrors()
->assertRedirect(route('studio.loyalty.voucher.index'));
$this->assertDatabaseHas('vouchers', [
'name' => 'Voucher Diskon 10rb',
'code' => 'DISKON10',
'tags' => 'Member Baru',
'type' => VoucherType::FIXED->value,
'discount_amount' => 10000,
'min_purchase' => 50000,
'quota' => 100,
'available_count' => 100,
'limit_per_user' => 2,
'summary' => 'Diskon 10rb untuk pembelian minimal 50rb',
'max_discount' => null,
]);
$voucher = Voucher::where('code', 'DISKON10')->first();
expect($voucher->outlets)->toHaveCount(3);
expect($voucher->outlets->pluck('id')->toArray())->toEqual($outletIds);
});
it('can create a fixed type voucher without optional fields', function () {
$outlets = createOutlets(1);
mountCreateComponent($this->user)
->set('form.name', 'Voucher Gratis')
->set('form.code', 'GRATIS')
->set('form.tags', 'Promo')
->set('form.type', VoucherType::FIXED->value)
->set('form.discount_amount', '5000')
->set('form.summary', 'Voucher gratis')
->set('form.start_date', now()->addDay()->format('Y-m-d'))
->set('form.outlet_ids', [$outlets->first()->id])
->call('save')
->assertHasNoErrors();
$this->assertDatabaseHas('vouchers', [
'code' => 'GRATIS',
'min_purchase' => null,
'max_discount' => null,
'quota' => null,
'available_count' => 0,
'limit_per_user' => null,
'end_date' => null,
]);
});
it('can create a fixed type voucher with currency formatted amounts', function () {
$outlets = createOutlets(1);
mountCreateComponent($this->user)
->set('form.name', 'Voucher Test')
->set('form.code', 'TEST01')
->set('form.tags', 'Test')
->set('form.type', VoucherType::FIXED->value)
->set('form.discount_amount', '25.000')
->set('form.min_purchase', '100.000')
->set('form.summary', 'Test voucher')
->set('form.start_date', now()->addDay()->format('Y-m-d'))
->set('form.outlet_ids', [$outlets->first()->id])
->call('save')
->assertHasNoErrors();
$this->assertDatabaseHas('vouchers', [
'code' => 'TEST01',
'discount_amount' => 25000,
'min_purchase' => 100000,
]);
});
/*
|--------------------------------------------------------------------------
| Create Voucher - Percentage Type
|--------------------------------------------------------------------------
*/
it('can create a percentage type voucher with max discount', function () {
$outlets = createOutlets(2);
mountCreateComponent($this->user)
->set('form.name', 'Diskon 20%')
->set('form.code', 'DISKON20')
->set('form.tags', 'Promo')
->set('form.type', VoucherType::PERCENTAGE->value)
->set('form.discount_amount', '20')
->set('form.max_discount', '50000')
->set('form.min_purchase', '100000')
->set('form.quota', '200')
->set('form.limit_per_user', '1')
->set('form.summary', 'Diskon 20% maksimal 50rb')
->set('form.start_date', now()->addDay()->format('Y-m-d'))
->set('form.end_date', now()->addMonths(2)->format('Y-m-d'))
->set('form.outlet_ids', $outlets->pluck('id')->toArray())
->call('save')
->assertHasNoErrors();
$this->assertDatabaseHas('vouchers', [
'code' => 'DISKON20',
'type' => VoucherType::PERCENTAGE->value,
'discount_amount' => 20,
'max_discount' => 50000,
'quota' => 200,
'available_count' => 200,
]);
});
it('requires max_discount for percentage type voucher', function () {
$outlets = createOutlets(1);
mountCreateComponent($this->user)
->set('form.name', 'Diskon Test')
->set('form.code', 'TEST02')
->set('form.tags', 'Test')
->set('form.type', VoucherType::PERCENTAGE->value)
->set('form.discount_amount', '15')
->set('form.summary', 'Test')
->set('form.start_date', now()->addDay()->format('Y-m-d'))
->set('form.outlet_ids', [$outlets->first()->id])
->call('save')
->assertHasErrors(['form.max_discount']);
});
/*
|--------------------------------------------------------------------------
| Validation Tests
|--------------------------------------------------------------------------
*/
it('validates required fields', function () {
mountCreateComponent($this->user)
->set('form.type', '')
->call('save')
->assertHasErrors([
'form.name',
'form.code',
'form.tags',
'form.type',
'form.discount_amount',
'form.summary',
'form.start_date',
'form.outlet_ids',
]);
});
it('validates name max length', function () {
$outlets = createOutlets(1);
mountCreateComponent($this->user)
->set('form.name', str_repeat('a', 51))
->set('form.code', 'TEST')
->set('form.tags', 'Test')
->set('form.type', VoucherType::FIXED->value)
->set('form.discount_amount', '10000')
->set('form.summary', 'Test')
->set('form.start_date', now()->addDay()->format('Y-m-d'))
->set('form.outlet_ids', [$outlets->first()->id])
->call('save')
->assertHasErrors(['form.name']);
});
it('validates code uniqueness', function () {
$voucher = Voucher::factory()->create(['code' => 'EXISTING']);
$outlets = createOutlets(1);
mountCreateComponent($this->user)
->set('form.name', 'New Voucher')
->set('form.code', 'EXISTING')
->set('form.tags', 'Test')
->set('form.type', VoucherType::FIXED->value)
->set('form.discount_amount', '10000')
->set('form.summary', 'Test')
->set('form.start_date', now()->addDay()->format('Y-m-d'))
->set('form.outlet_ids', [$outlets->first()->id])
->call('save')
->assertHasErrors(['form.code']);
});
it('validates code format (alpha_num)', function () {
$outlets = createOutlets(1);
mountCreateComponent($this->user)
->set('form.name', 'Test')
->set('form.code', 'TEST-CODE')
->set('form.tags', 'Test')
->set('form.type', VoucherType::FIXED->value)
->set('form.discount_amount', '10000')
->set('form.summary', 'Test')
->set('form.start_date', now()->addDay()->format('Y-m-d'))
->set('form.outlet_ids', [$outlets->first()->id])
->call('save')
->assertHasErrors(['form.code']);
});
it('validates tags max length', function () {
$outlets = createOutlets(1);
mountCreateComponent($this->user)
->set('form.name', 'Test')
->set('form.code', 'TEST')
->set('form.tags', str_repeat('a', 21))
->set('form.type', VoucherType::FIXED->value)
->set('form.discount_amount', '10000')
->set('form.summary', 'Test')
->set('form.start_date', now()->addDay()->format('Y-m-d'))
->set('form.outlet_ids', [$outlets->first()->id])
->call('save')
->assertHasErrors(['form.tags']);
});
it('validates percentage discount amount range', function () {
$outlets = createOutlets(1);
mountCreateComponent($this->user)
->set('form.name', 'Test')
->set('form.code', 'TEST')
->set('form.tags', 'Test')
->set('form.type', VoucherType::PERCENTAGE->value)
->set('form.discount_amount', '101')
->set('form.max_discount', '50000')
->set('form.summary', 'Test')
->set('form.start_date', now()->addDay()->format('Y-m-d'))
->set('form.outlet_ids', [$outlets->first()->id])
->call('save')
->assertHasErrors(['form.discount_amount']);
mountCreateComponent($this->user)
->set('form.name', 'Test')
->set('form.code', 'TEST2')
->set('form.tags', 'Test')
->set('form.type', VoucherType::PERCENTAGE->value)
->set('form.discount_amount', '-1')
->set('form.max_discount', '50000')
->set('form.summary', 'Test')
->set('form.start_date', now()->addDay()->format('Y-m-d'))
->set('form.outlet_ids', [$outlets->first()->id])
->call('save')
->assertHasErrors(['form.discount_amount']);
});
it('validates start date is after yesterday', function () {
$outlets = createOutlets(1);
mountCreateComponent($this->user)
->set('form.name', 'Test')
->set('form.code', 'TEST')
->set('form.tags', 'Test')
->set('form.type', VoucherType::FIXED->value)
->set('form.discount_amount', '10000')
->set('form.summary', 'Test')
->set('form.start_date', now()->subDays(2)->format('Y-m-d'))
->set('form.outlet_ids', [$outlets->first()->id])
->call('save')
->assertHasErrors(['form.start_date']);
});
it('validates end date is after or equal to start date', function () {
$outlets = createOutlets(1);
mountCreateComponent($this->user)
->set('form.name', 'Test')
->set('form.code', 'TEST')
->set('form.tags', 'Test')
->set('form.type', VoucherType::FIXED->value)
->set('form.discount_amount', '10000')
->set('form.summary', 'Test')
->set('form.start_date', now()->addDays(5)->format('Y-m-d'))
->set('form.end_date', now()->addDays(3)->format('Y-m-d'))
->set('form.outlet_ids', [$outlets->first()->id])
->call('save')
->assertHasErrors(['form.end_date']);
});
it('validates outlet_ids is required and has at least one', function () {
mountCreateComponent($this->user)
->set('form.name', 'Test')
->set('form.code', 'TEST')
->set('form.tags', 'Test')
->set('form.type', VoucherType::FIXED->value)
->set('form.discount_amount', '10000')
->set('form.summary', 'Test')
->set('form.start_date', now()->addDay()->format('Y-m-d'))
->set('form.outlet_ids', [])
->call('save')
->assertHasErrors(['form.outlet_ids']);
mountCreateComponent($this->user)
->set('form.name', 'Test')
->set('form.code', 'TEST2')
->set('form.tags', 'Test')
->set('form.type', VoucherType::FIXED->value)
->set('form.discount_amount', '10000')
->set('form.summary', 'Test')
->set('form.start_date', now()->addDay()->format('Y-m-d'))
->call('save')
->assertHasErrors(['form.outlet_ids']);
});
it('validates outlet_ids exist', function () {
mountCreateComponent($this->user)
->set('form.name', 'Test')
->set('form.code', 'TEST')
->set('form.tags', 'Test')
->set('form.type', VoucherType::FIXED->value)
->set('form.discount_amount', '10000')
->set('form.summary', 'Test')
->set('form.start_date', now()->addDay()->format('Y-m-d'))
->set('form.outlet_ids', [99999])
->call('save')
->assertHasErrors(['form.outlet_ids.0']);
});
it('validates summary max length', function () {
$outlets = createOutlets(1);
mountCreateComponent($this->user)
->set('form.name', 'Test')
->set('form.code', 'TEST')
->set('form.tags', 'Test')
->set('form.type', VoucherType::FIXED->value)
->set('form.discount_amount', '10000')
->set('form.summary', str_repeat('a', 201))
->set('form.start_date', now()->addDay()->format('Y-m-d'))
->set('form.outlet_ids', [$outlets->first()->id])
->call('save')
->assertHasErrors(['form.summary']);
});
/*
|--------------------------------------------------------------------------
| Outlet Selection
|--------------------------------------------------------------------------
*/
it('can select all outlets', function () {
$outlets = createOutlets(5);
$component = mountCreateComponent($this->user);
$component->call('selectAllOutlets');
expect($component->form->outlet_ids)->toHaveCount(5);
expect($component->form->outlet_ids)->toEqual($outlets->pluck('id')->toArray());
});
it('can deselect all outlets', function () {
$outlets = createOutlets(3);
$component = mountCreateComponent($this->user)
->set('form.outlet_ids', $outlets->pluck('id')->toArray());
expect($component->form->outlet_ids)->toHaveCount(3);
$component->call('deselectAllOutlets');
expect($component->form->outlet_ids)->toHaveCount(0);
expect($component->form->outlet_ids)->toBe([]);
});
/*
|--------------------------------------------------------------------------
| Permissions
|--------------------------------------------------------------------------
*/
it('prevents creation when user lacks create voucher permission', function () {
$this->ownerRole->revokePermissionTo('create voucher');
Gate::define('create voucher', fn () => false);
$outlets = createOutlets(1);
mountCreateComponent($this->user)
->set('form.name', 'Test')
->set('form.code', 'TEST')
->set('form.tags', 'Test')
->set('form.type', VoucherType::FIXED->value)
->set('form.discount_amount', '10000')
->set('form.summary', 'Test')
->set('form.start_date', now()->addDay()->format('Y-m-d'))
->set('form.outlet_ids', [$outlets->first()->id])
->call('save')
->assertForbidden();
});
it('allows creation when user has create voucher permission', function () {
$outlets = createOutlets(1);
mountCreateComponent($this->user)
->set('form.name', 'Test')
->set('form.code', 'TEST')
->set('form.tags', 'Test')
->set('form.type', VoucherType::FIXED->value)
->set('form.discount_amount', '10000')
->set('form.summary', 'Test')
->set('form.start_date', now()->addDay()->format('Y-m-d'))
->set('form.outlet_ids', [$outlets->first()->id])
->call('save')
->assertHasNoErrors();
});
/*
|--------------------------------------------------------------------------
| Push Notification
|--------------------------------------------------------------------------
*/
it('dispatches push notification job when creating voucher', function () {
$users = User::factory()->count(3)->create();
foreach ($users as $user) {
PushNotification::create([
'user_id' => $user->id,
'subscriptions' => json_encode(['endpoint' => 'test', 'keys' => ['p256dh' => 'test', 'auth' => 'test']]),
]);
}
$outlets = createOutlets(1);
mountCreateComponent($this->user)
->set('form.name', 'Test')
->set('form.code', 'TEST')
->set('form.tags', 'Test')
->set('form.type', VoucherType::FIXED->value)
->set('form.discount_amount', '10000')
->set('form.summary', 'Test')
->set('form.start_date', now()->addDay()->format('Y-m-d'))
->set('form.outlet_ids', [$outlets->first()->id])
->call('save');
Bus::assertDispatched(StorePushNotification::class, function ($job) {
return $job->payload['title'] === '🎁 Voucher Baru!' &&
$job->payload['body'] === 'Ada voucher baru untuk kamu! Jangan sampai kelewatan, yuk cek sekarang dan nikmati penawarannya ✨🎉';
});
});
/*
|--------------------------------------------------------------------------
| Edge Cases
|--------------------------------------------------------------------------
*/
it('handles null optional fields correctly', function () {
$outlets = createOutlets(1);
mountCreateComponent($this->user)
->set('form.name', 'Test')
->set('form.code', 'TEST')
->set('form.tags', 'Test')
->set('form.type', VoucherType::FIXED->value)
->set('form.discount_amount', '10000')
->set('form.min_purchase', null)
->set('form.quota', null)
->set('form.limit_per_user', null)
->set('form.summary', 'Test')
->set('form.start_date', now()->addDay()->format('Y-m-d'))
->set('form.end_date', null)
->set('form.outlet_ids', [$outlets->first()->id])
->call('save')
->assertHasNoErrors();
$voucher = Voucher::where('code', 'TEST')->first();
expect($voucher->min_purchase)->toBeNull();
expect($voucher->quota)->toBeNull();
expect($voucher->limit_per_user)->toBeNull();
expect($voucher->end_date)->toBeNull();
expect($voucher->available_count)->toBe(0);
});
it('sets available_count equal to quota when quota is provided', function () {
$outlets = createOutlets(1);
mountCreateComponent($this->user)
->set('form.name', 'Test')
->set('form.code', 'TEST')
->set('form.tags', 'Test')
->set('form.type', VoucherType::FIXED->value)
->set('form.discount_amount', '10000')
->set('form.quota', '150')
->set('form.summary', 'Test')
->set('form.start_date', now()->addDay()->format('Y-m-d'))
->set('form.outlet_ids', [$outlets->first()->id])
->call('save')
->assertHasNoErrors();
$voucher = Voucher::where('code', 'TEST')->first();
expect($voucher->quota)->toBe(150);
expect($voucher->available_count)->toBe(150);
});

View File

@ -1,495 +0,0 @@
<?php
use App\Enums\VoucherType;
use App\Livewire\Studio\Loyalty\Voucher\Edit;
use App\Models\Employee;
use App\Models\Facility;
use App\Models\OpeningHour;
use App\Models\Outlet;
use App\Models\User;
use App\Models\Voucher;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::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 mountEditComponent(User $user, Voucher $voucher)
{
return Livewire::actingAs($user)->test(Edit::class, ['voucher' => $voucher]);
}
function createTestOutlets(int $count = 3)
{
return Outlet::factory()
->count($count)
->has(OpeningHour::factory()->count(7))
->has(Facility::factory()->count(5))
->create();
}
/*
|--------------------------------------------------------------------------
| Component Rendering & Mount
|--------------------------------------------------------------------------
*/
it('renders page successfully', function () {
$voucher = Voucher::factory()->create();
$voucher->outlets()->attach(createOutlets(2)->pluck('id'));
mountEditComponent($this->user, $voucher)
->assertViewIs('livewire.studio.loyalty.voucher.form')
->assertViewHas('pageTitle', 'Ubah Voucher');
});
it('loads outlets on mount', function () {
$voucher = Voucher::factory()->create();
$outlets = createOutlets(3);
$component = mountEditComponent($this->user, $voucher);
expect($component->outlets)->toHaveCount(3);
expect($component->outlets)->toHaveKey($outlets->first()->id);
});
it('loads voucher data into form on mount', function () {
$outlets = createOutlets(2);
$voucher = Voucher::factory()->create([
'name' => 'Test Voucher',
'code' => 'TEST123',
'tags' => 'Promo',
'type' => VoucherType::FIXED->value,
'discount_amount' => 25000,
'min_purchase' => 100000,
'max_discount' => null,
'quota' => '50',
'limit_per_user' => '2',
'summary' => 'Test summary',
'start_date' => now()->addDay()->format('Y-m-d'),
'end_date' => now()->addMonth()->format('Y-m-d'),
]);
$voucher->outlets()->attach($outlets->pluck('id'));
$component = mountEditComponent($this->user, $voucher);
expect($component->form->name)->toBe('Test Voucher');
expect($component->form->code)->toBe('TEST123');
expect($component->form->tags)->toBe('Promo');
expect($component->form->type)->toBe((string) VoucherType::FIXED->value);
expect($component->form->summary)->toBe('Test summary');
expect($component->form->quota)->toBe('50');
expect($component->form->limit_per_user)->toBe('2');
expect($component->form->start_date)->toBe(now()->addDay()->format('Y-m-d'));
expect($component->form->end_date)->toBe(now()->addMonth()->format('Y-m-d'));
expect($component->form->outlet_ids)->toEqual($outlets->pluck('id')->toArray());
});
it('formats currency values correctly when loading voucher', function () {
$voucher = Voucher::factory()->create([
'discount_amount' => 50000,
'min_purchase' => 200000,
'max_discount' => 100000,
]);
$voucher->outlets()->attach(createOutlets(1)->pluck('id'));
$component = mountEditComponent($this->user, $voucher);
// Note: parseRupiahToInt returns integer, so we check the raw value
// The form will display formatted currency in the view
expect($component->form->discount_amount)->toBe('50000');
expect($component->form->min_purchase)->toBe('200000');
expect($component->form->max_discount)->toBe('100000');
});
/*
|--------------------------------------------------------------------------
| Update Voucher - Fixed Type
|--------------------------------------------------------------------------
*/
it('can update a fixed type voucher', function () {
$outlets = createOutlets(3);
$voucher = Voucher::factory()->create([
'type' => VoucherType::FIXED->value,
]);
$voucher->outlets()->attach($outlets->take(2)->pluck('id'));
$newOutlets = createOutlets(2);
$allOutlets = $outlets->merge($newOutlets);
mountEditComponent($this->user, $voucher)
->set('form.name', 'Updated Voucher')
->set('form.code', 'UPDATED')
->set('form.tags', 'Updated Tag')
->set('form.discount_amount', '30000')
->set('form.min_purchase', '150000')
->set('form.quota', '75')
->set('form.limit_per_user', '3')
->set('form.summary', 'Updated summary')
->set('form.start_date', now()->addDays(2)->format('Y-m-d'))
->set('form.end_date', now()->addMonths(2)->format('Y-m-d'))
->set('form.outlet_ids', $allOutlets->pluck('id')->toArray())
->call('save')
->assertHasNoErrors()
->assertRedirect(route('studio.loyalty.voucher.index'));
$voucher->refresh();
expect($voucher->name)->toBe('Updated Voucher');
expect($voucher->code)->toBe('UPDATED');
expect($voucher->tags)->toBe('Updated Tag');
expect($voucher->discount_amount)->toBe(30000);
expect($voucher->min_purchase)->toBe(150000);
expect($voucher->quota)->toBe(75);
expect($voucher->limit_per_user)->toBe(3);
expect($voucher->outlets)->toHaveCount(5);
});
it('can update voucher to remove optional fields', function () {
$voucher = Voucher::factory()->create([
'min_purchase' => 50000,
'quota' => 100,
'limit_per_user' => 2,
'end_date' => now()->addMonth()->format('Y-m-d'),
]);
$voucher->outlets()->attach(createOutlets(1)->pluck('id'));
mountEditComponent($this->user, $voucher)
->set('form.min_purchase', null)
->set('form.quota', null)
->set('form.limit_per_user', null)
->set('form.end_date', null)
->call('save')
->assertHasNoErrors();
$voucher->refresh();
expect($voucher->min_purchase)->toBeNull();
expect($voucher->quota)->toBeNull();
expect($voucher->limit_per_user)->toBeNull();
expect($voucher->end_date)->toBeNull();
});
/*
|--------------------------------------------------------------------------
| Update Voucher - Percentage Type
|--------------------------------------------------------------------------
*/
it('can update voucher from fixed to percentage type', function () {
$voucher = Voucher::factory()->create([
'type' => VoucherType::FIXED->value,
'discount_amount' => 10000,
'max_discount' => null,
]);
$voucher->outlets()->attach(createOutlets(1)->pluck('id'));
mountEditComponent($this->user, $voucher)
->set('form.type', VoucherType::PERCENTAGE->value)
->set('form.discount_amount', '15')
->set('form.max_discount', '40000')
->call('save')
->assertHasNoErrors();
$voucher->refresh();
expect($voucher->type)->toBe(VoucherType::PERCENTAGE);
expect($voucher->discount_amount)->toBe(15);
expect($voucher->max_discount)->toBe(40000);
});
it('can update voucher from percentage to fixed type', function () {
$voucher = Voucher::factory()->create([
'type' => VoucherType::PERCENTAGE->value,
'discount_amount' => 20,
'max_discount' => 50000,
]);
$voucher->outlets()->attach(createOutlets(1)->pluck('id'));
mountEditComponent($this->user, $voucher)
->set('form.type', VoucherType::FIXED->value)
->set('form.discount_amount', '25000')
->call('save')
->assertHasNoErrors();
$voucher->refresh();
expect($voucher->type)->toBe(VoucherType::FIXED);
expect($voucher->discount_amount)->toBe(25000);
expect($voucher->max_discount)->toBeNull();
});
it('can update percentage voucher with new max discount', function () {
$voucher = Voucher::factory()->create([
'type' => VoucherType::PERCENTAGE->value,
'discount_amount' => 10,
'max_discount' => 30000,
]);
$voucher->outlets()->attach(createOutlets(1)->pluck('id'));
mountEditComponent($this->user, $voucher)
->set('form.discount_amount', '25')
->set('form.max_discount', '60000')
->call('save')
->assertHasNoErrors();
$voucher->refresh();
expect($voucher->discount_amount)->toBe(25);
expect($voucher->max_discount)->toBe(60000);
});
/*
|--------------------------------------------------------------------------
| Outlet Management
|--------------------------------------------------------------------------
*/
it('can update voucher outlets', function () {
$originalOutlets = createOutlets(2);
$voucher = Voucher::factory()->create();
$voucher->outlets()->attach($originalOutlets->pluck('id'));
$newOutlets = createOutlets(3);
mountEditComponent($this->user, $voucher)
->set('form.outlet_ids', $newOutlets->pluck('id')->toArray())
->call('save')
->assertHasNoErrors();
$voucher->refresh();
expect($voucher->outlets)->toHaveCount(3);
expect($voucher->outlets->pluck('id')->toArray())->toEqual($newOutlets->pluck('id')->toArray());
});
it('can add more outlets to existing voucher', function () {
$originalOutlets = createOutlets(2);
$voucher = Voucher::factory()->create();
$voucher->outlets()->attach($originalOutlets->pluck('id'));
$additionalOutlets = createOutlets(2);
$allOutlets = $originalOutlets->merge($additionalOutlets);
mountEditComponent($this->user, $voucher)
->set('form.outlet_ids', $allOutlets->pluck('id')->toArray())
->call('save')
->assertHasNoErrors();
$voucher->refresh();
expect($voucher->outlets)->toHaveCount(4);
});
it('can remove all outlets from voucher', function () {
$outlets = createOutlets(3);
$voucher = Voucher::factory()->create();
$voucher->outlets()->attach($outlets->pluck('id'));
mountEditComponent($this->user, $voucher)
->set('form.outlet_ids', [])
->call('save')
->assertHasErrors(['form.outlet_ids']); // Should fail validation
});
it('can select all outlets when editing', function () {
$outlets = createOutlets(5);
$voucher = Voucher::factory()->create();
$voucher->outlets()->attach($outlets->take(2)->pluck('id'));
$component = mountEditComponent($this->user, $voucher);
$component->call('selectAllOutlets');
expect($component->form->outlet_ids)->toHaveCount(5);
expect($component->form->outlet_ids)->toEqual($outlets->pluck('id')->toArray());
});
it('can deselect all outlets when editing', function () {
$outlets = createOutlets(3);
$voucher = Voucher::factory()->create();
$voucher->outlets()->attach($outlets->pluck('id'));
$component = mountEditComponent($this->user, $voucher);
expect($component->form->outlet_ids)->toHaveCount(3);
$component->call('deselectAllOutlets');
expect($component->form->outlet_ids)->toHaveCount(0);
});
/*
|--------------------------------------------------------------------------
| Validation Tests
|--------------------------------------------------------------------------
*/
it('validates required fields on update', function () {
$voucher = Voucher::factory()->create();
$voucher->outlets()->attach(createOutlets(1)->pluck('id'));
mountEditComponent($this->user, $voucher)
->set('form.name', '')
->set('form.code', '')
->set('form.tags', '')
->set('form.summary', '')
->set('form.start_date', '')
->set('form.outlet_ids', [])
->call('save')
->assertHasErrors([
'form.name',
'form.code',
'form.tags',
'form.summary',
'form.start_date',
'form.outlet_ids',
]);
});
it('allows same code when updating voucher', function () {
$voucher = Voucher::factory()->create(['code' => 'EXISTING']);
$voucher->outlets()->attach(createOutlets(1)->pluck('id'));
mountEditComponent($this->user, $voucher)
->set('form.name', 'Updated Name')
->set('form.code', 'EXISTING') // Same code
->set('form.tags', 'Test')
->set('form.type', VoucherType::FIXED->value)
->set('form.discount_amount', '10000')
->set('form.summary', 'Test')
->set('form.start_date', now()->addDay()->format('Y-m-d'))
->set('form.outlet_ids', [$voucher->outlets->first()->id])
->call('save')
->assertHasNoErrors();
});
it('validates code uniqueness when changing to existing code', function () {
$voucher1 = Voucher::factory()->create(['code' => 'VOUCHER1']);
$voucher2 = Voucher::factory()->create(['code' => 'VOUCHER2']);
$voucher1->outlets()->attach(createOutlets(1)->pluck('id'));
mountEditComponent($this->user, $voucher1)
->set('form.code', 'VOUCHER2') // Trying to use existing code
->set('form.name', 'Test')
->set('form.tags', 'Test')
->set('form.type', VoucherType::FIXED->value)
->set('form.discount_amount', '10000')
->set('form.summary', 'Test')
->set('form.start_date', now()->addDay()->format('Y-m-d'))
->set('form.outlet_ids', [$voucher1->outlets->first()->id])
->call('save')
->assertHasErrors(['form.code']);
});
it('validates percentage discount requires max_discount on update', function () {
$voucher = Voucher::factory()->create([
'type' => VoucherType::PERCENTAGE->value,
'max_discount' => 50000,
]);
$voucher->outlets()->attach(createOutlets(1)->pluck('id'));
mountEditComponent($this->user, $voucher)
->set('form.max_discount', null)
->call('save')
->assertHasErrors(['form.max_discount']);
});
/*
|--------------------------------------------------------------------------
| Permissions
|--------------------------------------------------------------------------
*/
it('prevents update when user lacks update voucher permission', function () {
$this->ownerRole->revokePermissionTo('update voucher');
Gate::define('update voucher', fn () => false);
$voucher = Voucher::factory()->create();
$voucher->outlets()->attach(createOutlets(1)->pluck('id'));
mountEditComponent($this->user, $voucher)
->set('form.name', 'Updated')
->set('form.code', 'UPDATED')
->set('form.tags', 'Test')
->set('form.type', VoucherType::FIXED->value)
->set('form.discount_amount', '10000')
->set('form.summary', 'Test')
->set('form.start_date', now()->addDay()->format('Y-m-d'))
->set('form.outlet_ids', [$voucher->outlets->first()->id])
->call('save')
->assertForbidden();
});
it('allows update when user has update voucher permission', function () {
$voucher = Voucher::factory()->create();
$voucher->outlets()->attach(createOutlets(1)->pluck('id'));
mountEditComponent($this->user, $voucher)
->set('form.name', 'Updated')
->set('form.code', 'UPDATED')
->set('form.tags', 'Test')
->set('form.type', VoucherType::FIXED->value)
->set('form.discount_amount', '10000')
->set('form.summary', 'Test')
->set('form.start_date', now()->addDay()->format('Y-m-d'))
->set('form.outlet_ids', [$voucher->outlets->first()->id])
->call('save')
->assertHasNoErrors();
});
/*
|--------------------------------------------------------------------------
| Edge Cases
|--------------------------------------------------------------------------
*/
it('handles voucher with no outlets initially', function () {
$voucher = Voucher::factory()->create();
// No outlets attached
$outlets = createOutlets(2);
mountEditComponent($this->user, $voucher)
->set('form.name', 'Test')
->set('form.code', 'TEST')
->set('form.tags', 'Test')
->set('form.type', VoucherType::FIXED->value)
->set('form.discount_amount', '10000')
->set('form.summary', 'Test')
->set('form.start_date', now()->addDay()->format('Y-m-d'))
->set('form.outlet_ids', $outlets->pluck('id')->toArray())
->call('save')
->assertHasNoErrors();
$voucher->refresh();
expect($voucher->outlets)->toHaveCount(2);
});
it('preserves available_count when updating quota', function () {
$voucher = Voucher::factory()->create([
'quota' => 100,
'available_count' => 50, // Some vouchers already used
]);
$voucher->outlets()->attach(createOutlets(1)->pluck('id'));
// Note: The form doesn't update available_count, only quota
// This test verifies that available_count is not reset
mountEditComponent($this->user, $voucher)
->set('form.quota', '200')
->call('save')
->assertHasNoErrors();
$voucher->refresh();
expect($voucher->quota)->toBe(200);
// available_count should remain 50 (not reset to 200)
expect($voucher->available_count)->toBe(50);
});

View File

@ -1,312 +0,0 @@
<?php
use App\Livewire\Studio\Loyalty\Voucher\Index;
use App\Models\Employee;
use App\Models\User;
use App\Models\Voucher;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
use Livewire\Livewire;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::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 mountIndexComponent(User $user)
{
return Livewire::actingAs($user)->test(Index::class);
}
/*
|--------------------------------------------------------------------------
| Component Rendering & Mount
|--------------------------------------------------------------------------
*/
it('renders page successfully', function () {
mountIndexComponent($this->user)
->assertViewIs('livewire.studio.loyalty.voucher.index')
->assertViewHas('pageTitle', 'Voucher');
});
it('displays all vouchers', function () {
Voucher::factory()->count(10)->create();
$voucher = Voucher::first();
mountIndexComponent($this->user)
->assertSee($voucher->name);
});
/*
|--------------------------------------------------------------------------
| Statistics
|--------------------------------------------------------------------------
*/
it('displays correct total voucher count', function () {
Voucher::factory()->count(5)->create();
$component = mountIndexComponent($this->user);
expect($component->stats)->toHaveCount(4);
expect($component->stats[0]['title'])->toBe('Total Voucher');
expect($component->stats[0]['value'])->toBe(5);
});
it('displays correct active voucher count', function () {
// Active vouchers: start_date <= now AND (end_date is null OR end_date >= now)
Voucher::factory()->create([
'start_date' => now()->subDay()->format('Y-m-d'),
'end_date' => now()->addDay()->format('Y-m-d'),
]);
Voucher::factory()->create([
'start_date' => now()->subDay()->format('Y-m-d'),
'end_date' => null,
]);
Voucher::factory()->create([
'start_date' => now()->subWeek()->format('Y-m-d'),
'end_date' => now()->addWeek()->format('Y-m-d'),
]);
// Inactive vouchers
Voucher::factory()->create([
'start_date' => now()->subWeek()->format('Y-m-d'),
'end_date' => now()->subDay()->format('Y-m-d'),
]);
// Upcoming vouchers
Voucher::factory()->create([
'start_date' => now()->addDay()->format('Y-m-d'),
'end_date' => now()->addWeek()->format('Y-m-d'),
]);
$component = mountIndexComponent($this->user);
expect($component->stats[1]['title'])->toBe('Voucher Aktif');
expect($component->stats[1]['value'])->toBe(3);
});
it('displays correct inactive voucher count', function () {
// Inactive vouchers: end_date < now
Voucher::factory()->create([
'start_date' => now()->subWeek()->format('Y-m-d'),
'end_date' => now()->subDay()->format('Y-m-d'),
]);
Voucher::factory()->create([
'start_date' => now()->subMonth()->format('Y-m-d'),
'end_date' => now()->subWeek()->format('Y-m-d'),
]);
// Active vouchers
Voucher::factory()->create([
'start_date' => now()->subDay()->format('Y-m-d'),
'end_date' => now()->addDay()->format('Y-m-d'),
]);
// Upcoming vouchers
Voucher::factory()->create([
'start_date' => now()->addDay()->format('Y-m-d'),
'end_date' => now()->addWeek()->format('Y-m-d'),
]);
$component = mountIndexComponent($this->user);
expect($component->stats[2]['title'])->toBe('Voucher Tidak Aktif');
expect($component->stats[2]['value'])->toBe(2);
});
it('displays correct upcoming voucher count', function () {
// Upcoming vouchers: start_date > now
Voucher::factory()->create([
'start_date' => now()->addDay()->format('Y-m-d'),
'end_date' => now()->addWeek()->format('Y-m-d'),
]);
Voucher::factory()->create([
'start_date' => now()->addWeek()->format('Y-m-d'),
'end_date' => now()->addMonth()->format('Y-m-d'),
]);
// Active vouchers
Voucher::factory()->create([
'start_date' => now()->subDay()->format('Y-m-d'),
'end_date' => now()->addDay()->format('Y-m-d'),
]);
// Inactive vouchers
Voucher::factory()->create([
'start_date' => now()->subWeek()->format('Y-m-d'),
'end_date' => now()->subDay()->format('Y-m-d'),
]);
$component = mountIndexComponent($this->user);
expect($component->stats[3]['title'])->toBe('Voucher Akan Datang');
expect($component->stats[3]['value'])->toBe(2);
});
it('calculates stats correctly with mixed voucher statuses', function () {
// Create vouchers with different statuses
Voucher::factory()->count(3)->create([
'start_date' => now()->subDay()->format('Y-m-d'),
'end_date' => now()->addDay()->format('Y-m-d'),
]);
Voucher::factory()->count(2)->create([
'start_date' => now()->subWeek()->format('Y-m-d'),
'end_date' => now()->subDay()->format('Y-m-d'),
]);
Voucher::factory()->count(4)->create([
'start_date' => now()->addDay()->format('Y-m-d'),
'end_date' => now()->addWeek()->format('Y-m-d'),
]);
$component = mountIndexComponent($this->user);
expect($component->stats[0]['value'])->toBe(9); // Total
expect($component->stats[1]['value'])->toBe(3); // Active
expect($component->stats[2]['value'])->toBe(2); // Inactive
expect($component->stats[3]['value'])->toBe(4); // Upcoming
});
it('handles vouchers with null end_date in stats', function () {
Voucher::factory()->create([
'start_date' => now()->subDay()->format('Y-m-d'),
'end_date' => null, // Never expires
]);
Voucher::factory()->create([
'start_date' => now()->addDay()->format('Y-m-d'),
'end_date' => null, // Upcoming, never expires
]);
$component = mountIndexComponent($this->user);
expect($component->stats[1]['value'])->toBe(1); // Active (started, no end date)
expect($component->stats[3]['value'])->toBe(1); // Upcoming (not started yet)
});
/*
|--------------------------------------------------------------------------
| Delete Voucher
|--------------------------------------------------------------------------
*/
it('can delete a voucher', function () {
$voucher = Voucher::factory()->create();
mountIndexComponent($this->user)
->call('delete', $voucher)
->assertHasNoErrors()
->assertDispatched('refreshDatatable');
expect(Voucher::count())->toBe(0);
$this->assertSoftDeleted('vouchers', ['id' => $voucher->id]);
});
it('can delete multiple vouchers', function () {
$vouchers = Voucher::factory()->count(5)->create();
$component = mountIndexComponent($this->user);
foreach ($vouchers as $voucher) {
$component->call('delete', $voucher);
}
expect(Voucher::count())->toBe(0);
});
it('soft deletes voucher when deleting', function () {
$voucher = Voucher::factory()->create();
mountIndexComponent($this->user)
->call('delete', $voucher);
expect(Voucher::withTrashed()->count())->toBe(1);
expect(Voucher::count())->toBe(0);
expect($voucher->fresh()->trashed())->toBeTrue();
});
/*
|--------------------------------------------------------------------------
| Permissions
|--------------------------------------------------------------------------
*/
it('displays create button when user has create voucher permission', function () {
mountIndexComponent($this->user)
->assertSee('Tambah');
});
it('hides create button when user lacks create voucher permission', function () {
$this->ownerRole->revokePermissionTo('create voucher');
Gate::define('create voucher', fn () => false);
mountIndexComponent($this->user)
->assertDontSee('Tambah');
});
/*
|--------------------------------------------------------------------------
| Edge Cases
|--------------------------------------------------------------------------
*/
it('handles empty voucher list', function () {
$component = mountIndexComponent($this->user);
expect($component->stats[0]['value'])->toBe(0);
expect($component->stats[1]['value'])->toBe(0);
expect($component->stats[2]['value'])->toBe(0);
expect($component->stats[3]['value'])->toBe(0);
});
it('handles voucher with same start and end date', function () {
$today = now()->format('Y-m-d');
Voucher::factory()->create([
'start_date' => $today,
'end_date' => $today,
]);
$component = mountIndexComponent($this->user);
// Should be counted as active (start_date <= now AND end_date >= now)
expect($component->stats[1]['value'])->toBe(1);
});
it('updates stats when vouchers are deleted', function () {
$voucher1 = Voucher::factory()->create([
'start_date' => now()->subDay()->format('Y-m-d'),
'end_date' => now()->addDay()->format('Y-m-d'),
]);
$voucher2 = Voucher::factory()->create([
'start_date' => now()->addDay()->format('Y-m-d'),
'end_date' => now()->addWeek()->format('Y-m-d'),
]);
$component = mountIndexComponent($this->user);
expect($component->stats[0]['value'])->toBe(2);
expect($component->stats[1]['value'])->toBe(1);
expect($component->stats[3]['value'])->toBe(1);
$component->call('delete', $voucher1);
// Note: Stats are calculated in mount(), so they won't update automatically
// This test verifies the delete works, but stats would need remount to update
expect(Voucher::count())->toBe(1);
});

View File

@ -1,136 +0,0 @@
<?php
require_once __DIR__.'/TestHelpers.php';
use App\Livewire\Studio\Manage\Article\Create;
use App\Models\User;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Livewire\Livewire;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->seed(RolePermissionSeeder::class);
$this->user = User::factory()
->active()
->has(\App\Models\Employee::factory())
->create();
$this->ownerRole = Role::where('name', 'Owner')->first();
$this->user->roles()->attach($this->ownerRole->id);
Queue::fake();
});
function mountCreateComponent(User $user): \Livewire\Features\SupportTesting\Testable
{
return Livewire::actingAs($user)->test(Create::class);
}
/*
|--------------------------------------------------------------------------
| Component Rendering & Mount
|--------------------------------------------------------------------------
*/
it('renders page successfully', function () {
mountCreateComponent($this->user)
->assertViewIs('livewire.studio.manage.article.form')
->assertViewHas('pageTitle', 'Tambah Artikel');
});
it('mounts with form initialized', function () {
$component = mountCreateComponent($this->user);
expect($component->form)->toBeInstanceOf(\App\Livewire\Forms\Studio\Manage\ArticleForm::class);
});
/*
|--------------------------------------------------------------------------
| Authorization Tests
|--------------------------------------------------------------------------
*/
it('requires create article permission', function () {
// Temporarily remove the permission from the user
$this->ownerRole->revokePermissionTo('create article');
mountCreateComponent($this->user)
->call('save')
->assertForbidden();
// Restore the permission
$this->user->givePermissionTo('create article');
});
it('allows creation with proper permissions', function () {
mountCreateComponent($this->user)
->assertOk();
});
/*
|--------------------------------------------------------------------------
| Form Validation
|--------------------------------------------------------------------------
*/
it('validates form data before saving', function () {
mountCreateComponent($this->user)
->call('save')
->assertHasErrors([
'form.title' => 'required',
'form.excerpt' => 'required',
'form.content' => 'required',
'form.thumbnail' => 'required',
]);
});
// Validation is already tested in 'it validates form data before saving'
/*
|--------------------------------------------------------------------------
| Article Creation
|--------------------------------------------------------------------------
*/
// Article creation is tested functionally through the save method
/*
|--------------------------------------------------------------------------
| Component Structure
|--------------------------------------------------------------------------
*/
it('has form property', function () {
$component = mountCreateComponent($this->user);
expect($component->form)->toBeInstanceOf(\App\Livewire\Forms\Studio\Manage\ArticleForm::class);
});
it('has save method', function () {
$component = new Create;
expect(method_exists($component, 'save'))->toBeTrue();
});
/*
|--------------------------------------------------------------------------
| Component Title
|--------------------------------------------------------------------------
*/
it('has title attribute set', function () {
$reflection = new ReflectionClass(Create::class);
$attributes = $reflection->getAttributes();
$titleAttribute = collect($attributes)->first(function ($attribute) {
return $attribute->getName() === 'Livewire\\Attributes\\Title';
});
expect($titleAttribute)->not->toBeNull();
expect($titleAttribute->getArguments()[0])->toBe('Tambah Artikel');
});

View File

@ -1,177 +0,0 @@
<?php
require_once __DIR__.'/TestHelpers.php';
use App\Enums\ArticleStatus;
use App\Livewire\Studio\Manage\Article\Edit;
use App\Models\Article;
use App\Models\User;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->seed(RolePermissionSeeder::class);
$this->user = User::factory()
->active()
->has(\App\Models\Employee::factory())
->create();
$this->ownerRole = Role::where('name', 'Owner')->first();
$this->user->roles()->attach($this->ownerRole->id);
$this->article = Article::factory()->create([
'author_id' => $this->user->id,
'status' => ArticleStatus::DRAFT,
]);
});
function mountEditComponent(User $user, Article $article): \Livewire\Features\SupportTesting\Testable
{
return Livewire::actingAs($user)->test(Edit::class, ['article' => $article->hash]);
}
/*
|--------------------------------------------------------------------------
| Component Rendering & Mount
|--------------------------------------------------------------------------
*/
it('renders page successfully', function () {
mountEditComponent($this->user, $this->article)
->assertViewIs('livewire.studio.manage.article.form')
->assertViewHas('pageTitle', 'Ubah Artikel');
});
it('mounts with article data loaded', function () {
$component = mountEditComponent($this->user, $this->article);
expect($component->form)->toBeInstanceOf(\App\Livewire\Forms\Studio\Manage\ArticleForm::class);
expect($component->form->article)->toBeInstanceOf(Article::class);
expect($component->form->title)->toBe($this->article->title);
expect($component->form->excerpt)->toBe($this->article->excerpt);
expect($component->form->content)->toBe($this->article->content);
expect($component->form->status)->toBe((string) $this->article->status->value);
});
/*
|--------------------------------------------------------------------------
| Authorization Tests
|--------------------------------------------------------------------------
*/
it('requires update article permission', function () {
// Temporarily remove the permission from the user
$this->ownerRole->revokePermissionTo('update article');
mountEditComponent($this->user, $this->article)
->call('save')
->assertForbidden();
// Restore the permission
$this->user->givePermissionTo('update article');
});
it('allows editing with proper permissions', function () {
mountEditComponent($this->user, $this->article)
->assertOk();
});
/*
|--------------------------------------------------------------------------
| Form Validation
|--------------------------------------------------------------------------
*/
it('validates form data before saving', function () {
mountEditComponent($this->user, $this->article)
->set('form.title', '')
->set('form.excerpt', '')
->set('form.content', '')
->set('form.thumbnail', [])
->call('save')
->assertHasErrors([
'form.title' => 'required',
'form.excerpt' => 'required',
'form.content' => 'required',
'form.thumbnail' => 'required',
]);
});
it('validates title field', function () {
mountEditComponent($this->user, $this->article)
->set('form.title', '')
->call('save')
->assertHasErrors(['form.title' => 'required']);
});
it('validates excerpt field', function () {
mountEditComponent($this->user, $this->article)
->set('form.excerpt', '')
->call('save')
->assertHasErrors(['form.excerpt' => 'required']);
});
it('validates content field', function () {
mountEditComponent($this->user, $this->article)
->set('form.content', '')
->call('save')
->assertHasErrors(['form.content' => 'required']);
});
it('validates status field', function () {
mountEditComponent($this->user, $this->article)
->set('form.status', 'invalid_status')
->call('save')
->assertHasErrors(['form.status']);
});
/*
|--------------------------------------------------------------------------
| Article Update
|--------------------------------------------------------------------------
*/
// Article updates and events are tested functionally through the save method
/*
|--------------------------------------------------------------------------
| Component Title
|--------------------------------------------------------------------------
*/
it('has title attribute set', function () {
$reflection = new ReflectionClass(Edit::class);
$attributes = $reflection->getAttributes();
$titleAttribute = collect($attributes)->first(function ($attribute) {
return $attribute->getName() === 'Livewire\\Attributes\\Title';
});
expect($titleAttribute)->not->toBeNull();
expect($titleAttribute->getArguments()[0])->toBe('Ubah Artikel');
});
/*
|--------------------------------------------------------------------------
| Mount Method
|--------------------------------------------------------------------------
*/
// Mount method is tested through component mounting in other tests
/*
|--------------------------------------------------------------------------
| Error Handling
|--------------------------------------------------------------------------
*/
it('handles non-existent article gracefully', function () {
// This would typically throw an exception or redirect
// For now, we just verify the component can be instantiated
expect(true)->toBeTrue();
});

View File

@ -1,245 +0,0 @@
<?php
require_once __DIR__.'/TestHelpers.php';
use App\Enums\ArticleStatus;
use App\Livewire\Studio\Manage\Article\Index;
use App\Models\Article;
use App\Models\User;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->seed(RolePermissionSeeder::class);
$this->user = User::factory()
->active()
->has(\App\Models\Employee::factory())
->create();
$this->ownerRole = Role::where('name', 'Owner')->first();
$this->user->roles()->attach($this->ownerRole->id);
$this->article = Article::factory()->create([
'author_id' => $this->user->id,
'status' => ArticleStatus::PUBLISHED,
]);
});
function mountIndexComponent(User $user): \Livewire\Features\SupportTesting\Testable
{
return Livewire::actingAs($user)->test(Index::class);
}
/*
|--------------------------------------------------------------------------
| Component Rendering & Mount
|--------------------------------------------------------------------------
*/
it('renders page successfully', function () {
mountIndexComponent($this->user)
->assertViewIs('livewire.studio.manage.article.index')
->assertViewHas('pageTitle', 'Artikel');
});
it('mounts successfully', function () {
$component = mountIndexComponent($this->user);
expect($component->instance())->toBeInstanceOf(Index::class);
});
/*
|--------------------------------------------------------------------------
| Component Properties
|--------------------------------------------------------------------------
*/
it('has title attribute set', function () {
$reflection = new ReflectionClass(Index::class);
$attributes = $reflection->getAttributes();
$titleAttribute = collect($attributes)->first(function ($attribute) {
return $attribute->getName() === 'Livewire\\Attributes\\Title';
});
expect($titleAttribute)->not->toBeNull();
expect($titleAttribute->getArguments()[0])->toBe('Artikel');
});
/*
|--------------------------------------------------------------------------
| Delete Functionality
|--------------------------------------------------------------------------
*/
it('deletes article successfully', function () {
$articleToDelete = Article::factory()->create([
'author_id' => $this->user->id,
'status' => ArticleStatus::DRAFT,
]);
mountIndexComponent($this->user)
->call('delete', $articleToDelete)
->assertDispatched('refreshDatatable');
// Verify article is soft deleted
expect($articleToDelete->fresh()->trashed())->toBeTrue();
});
it('soft deletes article', function () {
$articleToDelete = Article::factory()->create([
'author_id' => $this->user->id,
'status' => ArticleStatus::DRAFT,
]);
mountIndexComponent($this->user)
->call('delete', $articleToDelete);
// Check if article exists with trashed
$deletedArticle = Article::withTrashed()->find($articleToDelete->id);
expect($deletedArticle)->not->toBeNull();
expect($deletedArticle->trashed())->toBeTrue();
});
/*
|--------------------------------------------------------------------------
| Delete Authorization
|--------------------------------------------------------------------------
*/
it('allows deletion with delete permission', function () {
$articleToDelete = Article::factory()->create([
'author_id' => $this->user->id,
'status' => ArticleStatus::DRAFT,
]);
mountIndexComponent($this->user)
->call('delete', $articleToDelete);
expect(Article::withTrashed()->find($articleToDelete->id)->trashed())->toBeTrue();
});
it('handles deletion of non-existent article gracefully', function () {
$nonExistentArticle = new Article;
$nonExistentArticle->id = 99999;
mountIndexComponent($this->user)
->call('delete', $nonExistentArticle)
->assertDispatched('refreshDatatable');
});
/*
|--------------------------------------------------------------------------
| Component Events
|--------------------------------------------------------------------------
*/
it('dispatches refreshDatatable event on delete', function () {
$articleToDelete = Article::factory()->create([
'author_id' => $this->user->id,
'status' => ArticleStatus::DRAFT,
]);
mountIndexComponent($this->user)
->call('delete', $articleToDelete)
->assertDispatched('refreshDatatable');
});
/*
|--------------------------------------------------------------------------
| Toast Messages
|--------------------------------------------------------------------------
*/
// Toast messages are tested functionally through the delete method
/*
|--------------------------------------------------------------------------
| Flux Modal Handling
|--------------------------------------------------------------------------
*/
it('closes flux modal after deletion', function () {
$articleToDelete = Article::factory()->create([
'author_id' => $this->user->id,
'status' => ArticleStatus::DRAFT,
]);
mountIndexComponent($this->user)
->call('delete', $articleToDelete);
// This would typically test Flux modal closure
// For now, we verify the method completes
expect(true)->toBeTrue();
});
/*
|--------------------------------------------------------------------------
| Traits Usage
|--------------------------------------------------------------------------
*/
it('uses WithConfirmation trait', function () {
$component = new Index;
expect(in_array('App\Traits\WithConfirmation', class_uses_recursive($component)))->toBeTrue();
});
it('uses WithSubscribeNotification trait', function () {
$component = new Index;
expect(in_array('App\Traits\Notification\WithSubscribeNotification', class_uses_recursive($component)))->toBeTrue();
});
it('uses WithToast trait', function () {
$component = new Index;
expect(in_array('App\Traits\WithToast', class_uses_recursive($component)))->toBeTrue();
});
/*
|--------------------------------------------------------------------------
| Render Method
|--------------------------------------------------------------------------
*/
it('has render method', function () {
$component = new Index;
expect(method_exists($component, 'render'))->toBeTrue();
});
it('render method returns correct view', function () {
$component = new Index;
$view = $component->render();
expect($view->getName())->toBe('livewire.studio.manage.article.index');
expect($view->getData()['pageTitle'])->toBe('Artikel');
});
/*
|--------------------------------------------------------------------------
| Component Structure
|--------------------------------------------------------------------------
*/
it('has delete method', function () {
$component = new Index;
expect(method_exists($component, 'delete'))->toBeTrue();
});
it('delete method requires article parameter', function () {
$component = new Index;
$reflection = new ReflectionMethod($component, 'delete');
$parameters = $reflection->getParameters();
expect(count($parameters))->toBe(1);
expect($parameters[0]->getType()->getName())->toBe(Article::class);
});

View File

@ -1,4 +0,0 @@
<?php
// Helper functions for Article tests
// Add helper functions here if needed in the future

View File

@ -1,198 +0,0 @@
<?php
use App\Livewire\Studio\Master\Formula;
use App\Models\Bottle;
use App\Models\Formula as FormulaModel;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
uses(RefreshDatabase::class);
beforeEach(function () {
// Setup Permissions
Permission::create(['name' => 'create formula']);
Permission::create(['name' => 'update formula']);
Permission::create(['name' => 'delete formula']);
});
function createAuthorizedUser()
{
$user = User::factory()->create();
$user->givePermissionTo(['create formula', 'update formula', 'delete formula']);
return $user;
}
it('renders successfully', function () {
$user = createAuthorizedUser();
$this->actingAs($user);
Bottle::factory()->create(['size' => 100]);
Livewire::test(Formula::class)
->assertStatus(200);
});
it('validates required fields', function () {
$user = createAuthorizedUser();
$this->actingAs($user);
Livewire::test(Formula::class)
->set('form.quality', '')
->set('form.size', '')
->set('form.volume', '')
->call('create')
->assertHasErrors(['form.quality', 'form.size', 'form.volume']);
});
it('validates quality max length', function () {
$user = createAuthorizedUser();
$this->actingAs($user);
Livewire::test(Formula::class)
->set('form.quality', str_repeat('a', 21))
->call('create')
->assertHasErrors(['form.quality']);
});
it('validates size numeric and min', function () {
$user = createAuthorizedUser();
$this->actingAs($user);
Livewire::test(Formula::class)
->set('form.size', 'abc')
->call('create')
->assertHasErrors(['form.size']);
Livewire::test(Formula::class)
->set('form.size', 0)
->call('create')
->assertHasErrors(['form.size']);
});
it('can create a formula', function () {
$user = createAuthorizedUser();
$this->actingAs($user);
Bottle::factory()->create(['size' => 100]);
Livewire::test(Formula::class)
->set('form.quality', 'Premium')
->set('form.size', '100')
->set('form.volume', '50')
->call('create');
$this->assertDatabaseHas('formulas', [
'quality' => 'Premium',
'size' => 100,
'volume' => 50,
]);
});
it('prevents duplicate formula creation', function () {
$user = createAuthorizedUser();
$this->actingAs($user);
FormulaModel::create([
'quality' => 'Premium',
'size' => 100,
'volume' => 50,
]);
Livewire::test(Formula::class)
->set('form.quality', 'Premium')
->set('form.size', '100')
->set('form.volume', '60')
->call('create');
expect(FormulaModel::count())->toBe(1);
});
it('can update a formula', function () {
$user = createAuthorizedUser();
$this->actingAs($user);
$formula = FormulaModel::create([
'quality' => 'Premium',
'size' => 100,
'volume' => 50,
]);
Livewire::test(Formula::class)
->call('openModal', 'update', 'Edit Formula', $formula->hash)
->assertSet('form.quality', 'Premium')
->assertSet('form.size', 100)
->set('form.quality', 'Super Premium')
->set('form.volume', '55')
->call('update');
$this->assertDatabaseHas('formulas', [
'id' => $formula->id,
'quality' => 'Super Premium',
'size' => 100,
'volume' => 55,
]);
});
it('prevents duplicate formula on update', function () {
$user = createAuthorizedUser();
$this->actingAs($user);
$formula1 = FormulaModel::create(['quality' => 'Premium', 'size' => 100, 'volume' => 50]);
$formula2 = FormulaModel::create(['quality' => 'Super Premium', 'size' => 100, 'volume' => 60]);
Livewire::test(Formula::class)
->call('openModal', 'update', 'Edit Formula', $formula1->hash)
->set('form.quality', 'Super Premium')
->set('form.size', '100')
->call('update');
$formula1->refresh();
expect($formula1->quality)->toBe('Premium');
});
it('can delete a formula', function () {
$user = createAuthorizedUser();
$this->actingAs($user);
$formula = FormulaModel::create(['quality' => 'Premium', 'size' => 100, 'volume' => 50]);
Livewire::test(Formula::class)
->call('delete', $formula->hash);
$this->assertSoftDeleted('formulas', ['id' => $formula->id]);
});
it('checks authorization for create', function () {
$user = User::factory()->create();
$this->actingAs($user);
Livewire::test(Formula::class)
->call('create')
->assertForbidden();
});
it('checks authorization for update', function () {
$user = User::factory()->create();
$this->actingAs($user);
$formula = FormulaModel::create(['quality' => 'Premium', 'size' => 100, 'volume' => 50]);
Livewire::test(Formula::class)
->call('openModal', 'update', 'Edit', $formula->hash)
->call('update')
->assertForbidden();
});
it('checks authorization for delete', function () {
$user = User::factory()->create();
$this->actingAs($user);
$formula = FormulaModel::create(['quality' => 'Premium', 'size' => 100, 'volume' => 50]);
Livewire::test(Formula::class)
->call('delete', $formula->hash)
->assertForbidden();
});

View File

@ -1,425 +0,0 @@
<?php
require_once __DIR__.'/TestHelpers.php';
use App\Enums\Day;
use App\Enums\OutletStatus;
use App\Livewire\Studio\Master\Outlet\Create;
use App\Livewire\Studio\Master\Outlet\Index;
use App\Models\Employee;
use App\Models\Facility;
use App\Models\Outlet;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->user = User::factory()
->active()
->has(Employee::factory())
->create();
});
function mountCreateComponent(User $user)
{
return Livewire::actingAs($user)->test(Create::class);
}
it('renders page successfully', function () {
mountCreateComponent($this->user)
->assertViewIs('livewire.studio.master.outlet.form')
->assertViewHas('pageTitle', 'Tambah Outlet');
});
it('mounts days and form opening_hours correctly', function () {
$component = mountCreateComponent($this->user);
$days = Day::cases();
expect($component->days)->toHaveCount(count($days));
foreach ($days as $day) {
expect(array_key_exists($day->value, $component->form->opening_hours))->toBeTrue();
expect($component->form->opening_hours[$day->value])->toBe(['open_time' => null, 'close_time' => null]);
}
});
it('prevents create when user is unauthorized', function () {
Gate::define('create outlet', fn () => false);
mountCreateComponent($this->user)
->call('save')
->assertForbidden();
});
it('saves outlet successfully when authorized', function () {
$permission = Permission::create(['name' => 'create outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->operational()->raw();
$facilities = Facility::factory()->count(5)->raw();
$featuredImage = makeLivewireFile('featured.jpg');
$images = [];
for ($i = 1; $i <= 5; $i++) {
$images[] = makeLivewireFile("image{$i}.jpg");
}
$openingHours = collect(Day::cases())
->mapWithKeys(fn ($day) => [
$day->value => [
'day' => $day->value,
'open_time' => fake()->time('H:i'),
'close_time' => fake()->time('H:i'),
],
])
->toArray();
$data = array_merge($outlet, [
'opening_hours' => $openingHours,
'facilities' => collect($facilities)->pluck('name')->toArray(),
'featured_image' => [$featuredImage],
'images' => $images,
]);
mountCreateComponent($this->user)
->set('form.name', $data['name'])
->set('form.phone_number', $data['phone_number'])
->set('form.address', $data['address'])
->set('form.landmark', $data['landmark'])
->set('form.maps_url', $data['maps_url'])
->set('form.opened_date', $data['opened_date'])
->set('form.status', $data['status']->value)
->set('form.opening_hours', $data['opening_hours'])
->set('form.facilities', $data['facilities'])
->set('form.featured_image', $data['featured_image'])
->set('form.images', $data['images'])
->call('save')
->assertHasNoErrors()
->assertRedirect(Index::class);
$outlet = Outlet::first();
$this->assertDatabaseHas('outlets', [
'id' => $outlet->id,
'name' => $data['name'],
'phone_number' => $data['phone_number'],
]);
$this->assertCount(1, $outlet->getMedia('featured_image'));
$this->assertCount(5, $outlet->getMedia('images'));
expect($outlet->getFirstMedia('featured_image')->getPath())->toBeFile();
foreach ($outlet->getMedia('images') as $media) {
expect($media->getPath())->toBeFile();
}
});
it('validates required fields', function () {
$permission = Permission::create(['name' => 'create outlet']);
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->call('save')
->assertHasErrors([
'form.name',
'form.phone_number',
'form.address',
'form.landmark',
'form.opened_date',
'form.featured_image',
'form.images',
]);
});
it('validates name field max length', function () {
$permission = Permission::create(['name' => 'create outlet']);
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->set('form.name', str_repeat('a', 101))
->call('save')
->assertHasErrors(['form.name']);
});
it('validates facilities minimum count', function () {
$permission = Permission::create(['name' => 'create outlet']);
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->set('form.facilities', [])
->call('save')
->assertHasErrors(['form.facilities']);
});
it('validates featured image max count', function () {
$permission = Permission::create(['name' => 'create outlet']);
$this->user->givePermissionTo($permission);
$featuredImages = [
makeLivewireFile('featured1.jpg'),
makeLivewireFile('featured2.jpg'),
];
mountCreateComponent($this->user)
->set('form.featured_image', $featuredImages)
->call('save')
->assertHasErrors(['form.featured_image']);
});
it('validates images max count', function () {
$permission = Permission::create(['name' => 'create outlet']);
$this->user->givePermissionTo($permission);
$images = [];
for ($i = 1; $i <= 6; $i++) {
$images[] = makeLivewireFile("image{$i}.jpg");
}
mountCreateComponent($this->user)
->set('form.images', $images)
->call('save')
->assertHasErrors(['form.images']);
});
it('has default facilities value', function () {
$component = mountCreateComponent($this->user);
expect($component->form->facilities)->toBe(['Parkir']);
});
it('initializes opening hours for all days of week', function () {
$component = mountCreateComponent($this->user);
foreach (Day::cases() as $day) {
expect(array_key_exists($day->value, $component->form->opening_hours))->toBeTrue();
expect($component->form->opening_hours[$day->value])->toHaveKeys(['open_time', 'close_time']);
expect($component->form->opening_hours[$day->value]['open_time'])->toBeNull();
expect($component->form->opening_hours[$day->value]['close_time'])->toBeNull();
}
});
it('creates outlet with opening hours for all days', function () {
$permission = Permission::create(['name' => 'create outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->operational()->raw();
$featuredImage = makeLivewireFile('featured.jpg');
$images = [makeLivewireFile('image1.jpg')];
$openingHours = collect(Day::cases())
->mapWithKeys(fn ($day) => [
$day->value => [
'open_time' => '09:00',
'close_time' => '21:00',
],
])
->toArray();
mountCreateComponent($this->user)
->set('form.name', $outlet['name'])
->set('form.phone_number', $outlet['phone_number'])
->set('form.address', $outlet['address'])
->set('form.landmark', $outlet['landmark'])
->set('form.opened_date', $outlet['opened_date'])
->set('form.status', $outlet['status']->value)
->set('form.opening_hours', $openingHours)
->set('form.facilities', ['Parkir'])
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasNoErrors();
$createdOutlet = Outlet::first();
expect($createdOutlet->openingHours)->toHaveCount(count(Day::cases()));
// Verify opening hours data
foreach ($createdOutlet->openingHours as $openingHour) {
expect($openingHour->open_time)->toBe('09:00');
expect($openingHour->close_time)->toBe('21:00');
}
});
it('creates outlet with facilities', function () {
$permission = Permission::create(['name' => 'create outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->operational()->raw();
$featuredImage = makeLivewireFile('featured.jpg');
$images = [makeLivewireFile('image1.jpg')];
$facilities = ['Parkir', 'WiFi', 'AC'];
mountCreateComponent($this->user)
->set('form.name', $outlet['name'])
->set('form.phone_number', $outlet['phone_number'])
->set('form.address', $outlet['address'])
->set('form.landmark', $outlet['landmark'])
->set('form.opened_date', $outlet['opened_date'])
->set('form.status', $outlet['status']->value)
->set('form.facilities', $facilities)
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasNoErrors();
$createdOutlet = Outlet::first();
expect($createdOutlet->facilities->pluck('name')->toArray())->toBe($facilities);
});
it('sets closed_date when status is TERMINATED', function () {
$permission = Permission::create(['name' => 'create outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->operational()->raw();
$featuredImage = makeLivewireFile('featured.jpg');
$images = [makeLivewireFile('image1.jpg')];
mountCreateComponent($this->user)
->set('form.name', $outlet['name'])
->set('form.phone_number', $outlet['phone_number'])
->set('form.address', $outlet['address'])
->set('form.landmark', $outlet['landmark'])
->set('form.opened_date', $outlet['opened_date'])
->set('form.status', OutletStatus::TERMINATED->value)
->set('form.facilities', ['Parkir'])
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasNoErrors();
$createdOutlet = Outlet::first();
expect($createdOutlet->status->value)->toBe(OutletStatus::TERMINATED->value);
expect($createdOutlet->closed_date)->not->toBeNull();
});
it('does not set closed_date for non-terminated status', function () {
$permission = Permission::create(['name' => 'create outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->operational()->raw();
$featuredImage = makeLivewireFile('featured.jpg');
$images = [makeLivewireFile('image1.jpg')];
mountCreateComponent($this->user)
->set('form.name', $outlet['name'])
->set('form.phone_number', $outlet['phone_number'])
->set('form.address', $outlet['address'])
->set('form.landmark', $outlet['landmark'])
->set('form.opened_date', $outlet['opened_date'])
->set('form.status', OutletStatus::OPERATIONAL->value)
->set('form.facilities', ['Parkir'])
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasNoErrors();
$createdOutlet = Outlet::first();
expect($createdOutlet->closed_date)->toBeNull();
});
it('can add and remove facilities dynamically', function () {
$component = mountCreateComponent($this->user);
// Initial state
expect($component->form->facilities)->toBe(['Parkir']);
// Add facility
$component->call('addFacility');
expect($component->form->facilities)->toBe(['Parkir', '']);
// Add another facility
$component->call('addFacility');
expect($component->form->facilities)->toBe(['Parkir', '', '']);
// Remove middle facility
$component->call('removeFacility', 1);
expect($component->form->facilities)->toBe(['Parkir', '']);
// Remove first facility (should keep at least one empty)
$component->call('removeFacility', 0);
expect($component->form->facilities)->toBe(['']);
});
it('validates maps_url regex pattern', function () {
$permission = Permission::create(['name' => 'create outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->operational()->raw();
$featuredImage = makeLivewireFile('featured.jpg');
$images = [makeLivewireFile('image1.jpg')];
mountCreateComponent($this->user)
->set('form.name', $outlet['name'])
->set('form.phone_number', $outlet['phone_number'])
->set('form.address', $outlet['address'])
->set('form.landmark', $outlet['landmark'])
->set('form.maps_url', 'invalid-url')
->set('form.opened_date', $outlet['opened_date'])
->set('form.facilities', ['Parkir'])
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasErrors(['form.maps_url']);
// Valid Google Maps URL should pass
mountCreateComponent($this->user)
->set('form.name', $outlet['name'])
->set('form.phone_number', $outlet['phone_number'])
->set('form.address', $outlet['address'])
->set('form.landmark', $outlet['landmark'])
->set('form.maps_url', 'https://maps.app.goo.gl/example')
->set('form.opened_date', $outlet['opened_date'])
->set('form.facilities', ['Parkir'])
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasNoErrors();
});
it('handles facility removal edge cases', function () {
$component = mountCreateComponent($this->user);
// Set custom facilities
$component->set('form.facilities', ['Parkir', 'WiFi', 'AC']);
// Remove middle facility
$component->call('removeFacility', 1);
expect($component->form->facilities)->toBe(['Parkir', 'AC']);
// Try to remove non-existent index (should not crash)
$component->call('removeFacility', 99);
expect($component->form->facilities)->toBe(['Parkir', 'AC']);
});
it('creates outlet with soft delete capability', function () {
$permission = Permission::create(['name' => 'create outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->operational()->raw();
$featuredImage = makeLivewireFile('featured.jpg');
$images = [makeLivewireFile('image1.jpg')];
mountCreateComponent($this->user)
->set('form.name', $outlet['name'])
->set('form.phone_number', $outlet['phone_number'])
->set('form.address', $outlet['address'])
->set('form.landmark', $outlet['landmark'])
->set('form.opened_date', $outlet['opened_date'])
->set('form.facilities', ['Parkir'])
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasNoErrors();
$createdOutlet = Outlet::first();
expect($createdOutlet)->toBeInstanceOf(Outlet::class);
// Verify soft delete trait
$createdOutlet->delete();
expect(Outlet::withTrashed()->find($createdOutlet->id))->not->toBeNull();
expect(Outlet::find($createdOutlet->id))->toBeNull();
});

View File

@ -1,346 +0,0 @@
<?php
require_once __DIR__.'/TestHelpers.php';
use App\Enums\Day;
use App\Enums\OutletStatus;
use App\Livewire\Studio\Master\Outlet\Edit;
use App\Livewire\Studio\Master\Outlet\Index;
use App\Models\Employee;
use App\Models\Facility;
use App\Models\OpeningHour;
use App\Models\Outlet;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->user = User::factory()
->active()
->has(Employee::factory())
->create();
});
function mountEditComponent(User $user, Outlet $outlet)
{
return Livewire::actingAs($user)->test(Edit::class, ['outlet' => $outlet]);
}
it('renders page successfully', function () {
$outlet = Outlet::factory()->operational()->create();
mountEditComponent($this->user, $outlet)
->assertViewIs('livewire.studio.master.outlet.form')
->assertViewHas('pageTitle', 'Ubah Outlet');
});
it('mounts days and sets form with outlet', function () {
$outlet = Outlet::factory()->operational()->create();
$component = mountEditComponent($this->user, $outlet);
$days = Day::cases();
expect($component->days)->toHaveCount(count($days));
expect($component->form->outlet->id)->toBe($outlet->id);
});
it('prevents update when user is unauthorized', function () {
$outlet = Outlet::factory()->operational()->create();
Gate::define('update outlet', fn () => false);
mountEditComponent($this->user, $outlet)
->call('save')
->assertForbidden();
});
it('updates outlet successfully when authorized', function () {
$permission = Permission::create(['name' => 'update outlet']);
$this->user->givePermissionTo($permission);
$outletModel = Outlet::factory()->operational()->create(); // model instance
$outletData = Outlet::factory()->operational()->raw(); // data for update
$facilities = Facility::factory()->count(5)->raw();
$featuredImage = makeLivewireFile('featured.jpg');
for ($i = 1; $i <= 5; $i++) {
$images[] = makeLivewireFile("image{$i}.jpg");
}
$openingHours = collect(Day::cases())
->mapWithKeys(fn ($day) => [
$day->value => [
'day' => $day->value,
'open_time' => fake()->time('H:i'),
'close_time' => fake()->time('H:i'),
],
])
->toArray();
$data = array_merge($outletData, [
'opening_hours' => $openingHours,
'facilities' => collect($facilities)->pluck('name')->toArray(),
'featured_image' => [$featuredImage],
'images' => $images,
]);
mountEditComponent($this->user, $outletModel)
->set('form.name', $data['name'])
->set('form.phone_number', $data['phone_number'])
->set('form.address', $data['address'])
->set('form.landmark', $data['landmark'])
->set('form.maps_url', $data['maps_url'])
->set('form.opened_date', $data['opened_date'])
->set('form.status', $data['status']->value)
->set('form.opening_hours', $data['opening_hours'])
->set('form.facilities', $data['facilities'])
->set('form.featured_image', $data['featured_image'])
->set('form.images', $data['images'])
->call('save')
->assertHasNoErrors()
->assertRedirect(Index::class);
$outletModel->refresh();
$this->assertDatabaseHas('outlets', [
'id' => $outletModel->id,
'name' => $data['name'],
'phone_number' => $data['phone_number'],
]);
$this->assertCount(1, $outletModel->getMedia('featured_image'));
$this->assertCount(5, $outletModel->getMedia('images'));
expect($outletModel->getFirstMedia('featured_image')->getPath())->toBeFile();
foreach ($outletModel->getMedia('images') as $media) {
expect($media->getPath())->toBeFile();
}
});
it('loads outlet data into form correctly', function () {
$outlet = Outlet::factory()
->operational()
->has(OpeningHour::factory()->count(7))
->has(Facility::factory()->count(3))
->create();
$component = mountEditComponent($this->user, $outlet);
expect($component->form->outlet->id)->toBe($outlet->id);
expect($component->form->name)->toBe($outlet->name);
expect($component->form->phone_number)->toBe($outlet->phone_number);
expect($component->form->address)->toBe($outlet->address);
expect($component->form->landmark)->toBe($outlet->landmark);
expect($component->form->status)->toBe($outlet->status->value);
expect($component->form->facilities)->toBe($outlet->facilities->pluck('name')->toArray());
});
it('loads opening hours into form correctly', function () {
$outlet = Outlet::factory()->operational()->create();
// Create specific opening hours
$openingHours = [];
foreach (Day::cases() as $day) {
$openingHours[] = OpeningHour::factory()->create([
'outlet_id' => $outlet->id,
'day' => $day,
'open_time' => '09:00',
'close_time' => '21:00',
]);
}
$component = mountEditComponent($this->user, $outlet);
foreach (Day::cases() as $day) {
expect($component->form->opening_hours[$day->value]['open_time'])->toBe('09:00');
expect($component->form->opening_hours[$day->value]['close_time'])->toBe('21:00');
}
});
it('updates outlet with partial data changes', function () {
$permission = Permission::create(['name' => 'update outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()
->operational()
->has(Facility::factory()->count(2))
->create();
$newName = 'Updated Outlet Name';
$images = [makeLivewireFile('image1.jpg')];
$featuredImage = makeLivewireFile('featured.jpg');
mountEditComponent($this->user, $outlet)
->set('form.name', $newName)
->set('form.opening_hours', makeValidOpeningHours())
->set('form.facilities', ['Parkir'])
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasNoErrors()
->assertRedirect(Index::class);
$outlet->refresh();
expect($outlet->name)->toBe($newName);
expect($outlet->phone_number)->toBe($outlet->phone_number); // Unchanged
});
it('updates outlet status to TERMINATED and sets closed_date', function () {
$permission = Permission::create(['name' => 'update outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->operational()->create();
$featuredImage = makeLivewireFile('featured.jpg');
$images = [makeLivewireFile('image1.jpg')];
mountEditComponent($this->user, $outlet)
->set('form.status', OutletStatus::TERMINATED->value)
->set('form.opening_hours', makeValidOpeningHours())
->set('form.facilities', ['Parkir'])
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasNoErrors();
$outlet->refresh();
expect($outlet->status->value)->toBe(OutletStatus::TERMINATED->value);
expect($outlet->closed_date)->not->toBeNull();
});
it('updates facilities correctly', function () {
$permission = Permission::create(['name' => 'update outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()
->operational()
->has(Facility::factory()->count(2))
->create();
$featuredImage = makeLivewireFile('featured.jpg');
$images = [makeLivewireFile('image1.jpg')];
$newFacilities = ['WiFi', 'AC', 'Parking'];
mountEditComponent($this->user, $outlet)
->set('form.facilities', $newFacilities)
->set('form.opening_hours', makeValidOpeningHours())
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasNoErrors();
$outlet->refresh();
expect($outlet->facilities->pluck('name')->toArray())->toBe($newFacilities);
});
it('updates opening hours correctly', function () {
$permission = Permission::create(['name' => 'update outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->operational()->create();
$featuredImage = makeLivewireFile('featured.jpg');
$images = [makeLivewireFile('image1.jpg')];
$newOpeningHours = makeValidOpeningHours();
mountEditComponent($this->user, $outlet)
->set('form.opening_hours', $newOpeningHours)
->set('form.facilities', ['Parkir'])
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasNoErrors();
$outlet->refresh();
expect($outlet->openingHours)->toHaveCount(count(Day::cases()));
foreach ($outlet->openingHours as $openingHour) {
expect($openingHour->open_time)->toBe('09:00');
expect($openingHour->close_time)->toBe('21:00');
}
});
it('validates required fields on update', function () {
$permission = Permission::create(['name' => 'update outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->operational()->create();
mountEditComponent($this->user, $outlet)
->set('form.name', '') // Empty required field
->call('save')
->assertHasErrors(['form.name']);
});
it('maintains outlet relationships integrity', function () {
$permission = Permission::create(['name' => 'update outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()
->operational()
->has(Facility::factory()->count(3))
->create();
$originalOpeningHoursCount = count(Day::cases()); // Should be 7 after setOutlet
$originalFacilitiesCount = 1; // We set facilities to ['Parkir'] in the test
$featuredImage = makeLivewireFile('featured.jpg');
$images = [makeLivewireFile('image1.jpg')];
mountEditComponent($this->user, $outlet)
->set('form.name', 'Updated Name')
->set('form.opening_hours', makeValidOpeningHours())
->set('form.facilities', ['Parkir'])
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasNoErrors();
$outlet->refresh();
expect($outlet->openingHours)->toHaveCount($originalOpeningHoursCount);
expect($outlet->facilities)->toHaveCount($originalFacilitiesCount);
});
it('handles outlet with no existing media', function () {
$permission = Permission::create(['name' => 'update outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->operational()->create();
$featuredImage = makeLivewireFile('featured.jpg');
$images = [makeLivewireFile('image1.jpg')];
mountEditComponent($this->user, $outlet)
->set('form.opening_hours', makeValidOpeningHours())
->set('form.facilities', ['Parkir'])
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasNoErrors();
$outlet->refresh();
expect($outlet->getMedia('featured_image'))->toHaveCount(1);
});
it('preserves outlet slug when updating', function () {
$permission = Permission::create(['name' => 'update outlet']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->operational()->create();
$originalSlug = $outlet->slug;
$featuredImage = makeLivewireFile('featured.jpg');
$images = [makeLivewireFile('image1.jpg')];
mountEditComponent($this->user, $outlet)
->set('form.phone_number', '0812 3456 7890') // Change non-slug field
->set('form.opening_hours', makeValidOpeningHours())
->set('form.facilities', ['Parkir'])
->set('form.featured_image', [$featuredImage])
->set('form.images', $images)
->call('save')
->assertHasNoErrors();
$outlet->refresh();
expect($outlet->slug)->toBe($originalSlug);
});

View File

@ -1,239 +0,0 @@
<?php
use App\Enums\OutletStatus;
use App\Livewire\Studio\Master\Outlet\Index;
use App\Models\Employee;
use App\Models\Facility;
use App\Models\OpeningHour;
use App\Models\Outlet;
use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
use Livewire\Features\SupportTesting\Testable;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->user = User::factory()
->active()
->has(Employee::factory())
->create();
});
function createOutlet(array $attributes = []): Outlet
{
return Outlet::factory()
->has(OpeningHour::factory()->count(7))
->has(Facility::factory()->count(5))
->state(fn () => array_merge([
'status' => fake()->randomElement(OutletStatus::cases()),
], $attributes))
->create();
}
function mountIndexComponent(User $user): Testable
{
return Livewire::actingAs($user)->test(Index::class);
}
it('renders page successfully', function () {
mountIndexComponent($this->user)
->assertViewIs('livewire.studio.master.outlet.index')
->assertViewHas('pageTitle', 'Outlet');
});
it('loads outlets as Eloquent collection', function () {
$outlet = createOutlet();
$component = mountIndexComponent($this->user);
expect($component->get('outlets'))
->toBeInstanceOf(Collection::class)
->and($component->get('outlets')->first()->id)->toBe($outlet->id);
});
it('loads outlets with relationships', function () {
$outlet = createOutlet();
$component = mountIndexComponent($this->user);
$loadedOutlet = $component->get('outlets')->first();
expect($loadedOutlet->openingHours)->toHaveCount(7)
->and($loadedOutlet->facilities)->toHaveCount(5);
});
it('displays all outlets', function () {
for ($i = 0; $i < 5; $i++) {
createOutlet();
}
$component = mountIndexComponent($this->user);
expect($component->get('outlets'))->toHaveCount(5);
expect($component->get('outlets')->first())->toBeInstanceOf(Outlet::class);
});
it('prevents delete when user is unauthorized', function () {
$outlet = createOutlet();
Gate::define('delete outlet', fn () => false);
mountIndexComponent($this->user)
->call('delete', $outlet)
->assertForbidden();
});
it('deletes an outlet successfully when authorized', function () {
$permission = Permission::create(['name' => 'delete outlet']);
$this->user->givePermissionTo($permission);
$outlet = createOutlet();
mountIndexComponent($this->user)
->call('delete', $outlet)
->assertHasNoErrors();
$this->assertSoftDeleted('outlets', ['id' => $outlet->id]);
});
it('filters outlets by search query', function () {
$outlet1 = createOutlet(['name' => 'Outlet Jakarta']);
$outlet2 = createOutlet(['name' => 'Outlet Bandung']);
$outlet3 = createOutlet(['name' => 'Toko Surabaya']);
$component = mountIndexComponent($this->user);
expect($component->get('outlets'))->toHaveCount(3);
// Test filtering by Jakarta
$component->set('search', 'Jakarta');
$filteredComponent = mountIndexComponent($this->user)->set('search', 'Jakarta');
expect($filteredComponent->get('outlets'))->toHaveCount(1);
expect($filteredComponent->get('outlets')->first()->name)->toBe('Outlet Jakarta');
// Test filtering by Outlet
$filteredComponent2 = mountIndexComponent($this->user)->set('search', 'Outlet');
expect($filteredComponent2->get('outlets'))->toHaveCount(2);
});
it('filters outlets by status', function () {
$outlet1 = createOutlet(['status' => OutletStatus::OPERATIONAL]);
$outlet2 = createOutlet(['status' => OutletStatus::UNDER_CONSTRUCTION]);
$outlet3 = createOutlet(['status' => OutletStatus::TERMINATED]);
$component = mountIndexComponent($this->user);
expect($component->get('outlets'))->toHaveCount(3);
$filteredComponent = mountIndexComponent($this->user)->set('status', [OutletStatus::OPERATIONAL->value]);
expect($filteredComponent->get('outlets'))->toHaveCount(1);
expect($filteredComponent->get('outlets')->first()->name)->toBe($outlet1->name);
// Test filtering by multiple statuses
$filteredComponent2 = mountIndexComponent($this->user)->set('status', [
OutletStatus::OPERATIONAL->value,
OutletStatus::UNDER_CONSTRUCTION->value,
]);
expect($filteredComponent2->get('outlets'))->toHaveCount(2);
});
it('filters outlets by both search and status', function () {
$outlet1 = createOutlet(['name' => 'Outlet A', 'status' => OutletStatus::OPERATIONAL]);
$outlet2 = createOutlet(['name' => 'Outlet B', 'status' => OutletStatus::OPERATIONAL]);
$outlet3 = createOutlet(['name' => 'Outlet C', 'status' => OutletStatus::UNDER_CONSTRUCTION]);
$component = mountIndexComponent($this->user)
->set('search', 'Outlet')
->set('status', [OutletStatus::OPERATIONAL->value]);
expect($component->get('outlets'))->toHaveCount(2);
});
it('loads outlets with all required relationships', function () {
$outlet = createOutlet();
$component = mountIndexComponent($this->user);
$loadedOutlet = $component->get('outlets')->first();
expect($loadedOutlet->openingHours)->toHaveCount(7)
->and($loadedOutlet->facilities)->toHaveCount(5)
->and($loadedOutlet->name)->toBe($outlet->name)
->and($loadedOutlet->phone_number)->toBe($outlet->phone_number);
});
it('loads outlets ordered by latest', function () {
$outlet1 = createOutlet(['name' => 'First']);
sleep(1);
$outlet2 = createOutlet(['name' => 'Second']);
$component = mountIndexComponent($this->user);
expect($component->get('outlets')->first()->name)->toBe('Second');
expect($component->get('outlets')->last()->name)->toBe('First');
});
it('provides helper methods for view formatting', function () {
$outlet = createOutlet();
$component = mountIndexComponent($this->user);
// Test that outlets are loaded with proper data structure
$loadedOutlet = $component->get('outlets')->first();
expect($loadedOutlet)->toBeInstanceOf(Outlet::class);
expect($loadedOutlet->openingHours)->toBeInstanceOf(Collection::class);
expect($loadedOutlet->facilities)->toBeInstanceOf(Collection::class);
});
it('handles empty search results gracefully', function () {
createOutlet(['name' => 'Existing Outlet']);
$component = mountIndexComponent($this->user)->set('search', 'NonExistentOutlet');
expect($component->get('outlets'))->toHaveCount(0);
});
it('maintains search state across component lifecycle', function () {
$outlet1 = createOutlet(['name' => 'Test Outlet']);
$outlet2 = createOutlet(['name' => 'Another Outlet']);
$component = mountIndexComponent($this->user)->set('search', 'Test');
expect($component->get('outlets'))->toHaveCount(1);
expect($component->get('search'))->toBe('Test');
// Re-mount should maintain state
$newComponent = mountIndexComponent($this->user)->set('search', 'Test');
expect($newComponent->get('outlets'))->toHaveCount(1);
});
it('handles status filter with empty array', function () {
createOutlet(['status' => OutletStatus::OPERATIONAL]);
createOutlet(['status' => OutletStatus::TERMINATED]);
$component = mountIndexComponent($this->user)->set('status', []);
expect($component->get('outlets'))->toHaveCount(2);
});
it('loads outlets efficiently with eager loading', function () {
$outlets = collect();
for ($i = 0; $i < 10; $i++) {
$outlets->push(createOutlet());
}
$component = mountIndexComponent($this->user);
// Verify relationships are loaded
$loadedOutlets = $component->get('outlets');
foreach ($loadedOutlets as $loadedOutlet) {
expect($loadedOutlet->relationLoaded('openingHours'))->toBeTrue();
expect($loadedOutlet->relationLoaded('facilities'))->toBeTrue();
}
});

View File

@ -1,37 +0,0 @@
<?php
use App\Enums\Day;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
function makeLivewireFile(string $name)
{
$file = UploadedFile::fake()->image($name, 100, 100);
$tmpId = Str::random(32);
$filename = $tmpId.'-'.base64_encode($file->getClientOriginalName()).'.'.$file->getClientOriginalExtension();
$path = "livewire-tmp/{$filename}";
Storage::disk('local')->putFileAs('livewire-tmp', $file, $filename);
return [
'tmpFilename' => $filename,
'name' => $file->getClientOriginalName(),
'extension' => $file->getClientOriginalExtension(),
'path' => storage_path("app/private/{$path}"),
'temporaryUrl' => url("livewire/preview-file/{$filename}"),
'size' => $file->getSize(),
];
}
function makeValidOpeningHours()
{
return collect(Day::cases())
->mapWithKeys(fn ($day) => [
$day->value => [
'open_time' => '09:00',
'close_time' => '21:00',
],
])
->toArray();
}

View File

@ -1,651 +0,0 @@
<?php
use App\Enums\Gender;
use App\Enums\UserStatus;
use App\Livewire\Studio\Master\User\Create;
use App\Livewire\Studio\Master\User\Index;
use App\Models\Employee;
use App\Models\Outlet;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Hash;
use Livewire\Features\SupportTesting\Testable;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->user = User::factory()
->active()
->has(Employee::factory())
->create();
});
function mountCreateComponent(User $user): Testable
{
return Livewire::actingAs($user)->test(Create::class);
}
it('renders page successfully', function () {
mountCreateComponent($this->user)
->assertViewIs('livewire.studio.master.user.form')
->assertViewHas('pageTitle', 'Tambah Pegawai');
});
it('loads outlets and roles on mount', function () {
$outlet = Outlet::factory()->create();
$role = Role::create(['name' => 'Admin']);
$component = mountCreateComponent($this->user);
expect($component->outlets)->toBeArray();
expect($component->outlets)->toHaveKey($outlet->id);
expect($component->roles)->toBeArray();
expect($component->roles)->toHaveKey($role->id);
});
it('excludes developer role from roles list', function () {
Role::create(['name' => 'Developer']);
$adminRole = Role::create(['name' => 'Admin']);
$component = mountCreateComponent($this->user);
expect($component->roles)->not->toHaveKey(Role::where('name', 'Developer')->first()->id);
expect($component->roles)->toHaveKey($adminRole->id);
});
it('prevents create when user is unauthorized', function () {
Gate::define('create user', fn () => false);
mountCreateComponent($this->user)
->call('save')
->assertForbidden();
});
it('creates user successfully when authorized', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->create();
$role = Role::create(['name' => 'Admin']);
$defaultPassword = config('myconfig.password_default');
mountCreateComponent($this->user)
->set('form.full_name', 'John Doe')
->set('form.username', 'johndoe')
->set('form.email', 'john@example.com')
->set('form.phone_number', '0812 3456 7890')
->set('form.base_salary', '5000000')
->set('form.birthdate', now()->subYears(25)->format('Y-m-d'))
->set('form.hire_date', now()->subMonths(6)->format('Y-m-d'))
->set('form.gender', Gender::MALE->value)
->set('form.status', UserStatus::ACTIVE->value)
->set('form.outlet_ids', [$outlet->id])
->set('form.role_ids', [$role->id])
->call('save')
->assertHasNoErrors()
->assertRedirect(Index::class);
$createdUser = User::where('email', 'john@example.com')->first();
expect($createdUser)->not->toBeNull()
->and($createdUser->username)->toBe('johndoe')
->and($createdUser->status)->toBe(UserStatus::ACTIVE)
->and(Hash::check($defaultPassword, $createdUser->password))->toBeTrue();
$this->assertDatabaseHas('employees', [
'user_id' => $createdUser->id,
'full_name' => 'John Doe',
'gender' => Gender::MALE,
]);
expect($createdUser->outlets)->toHaveCount(1)
->and($createdUser->roles)->toHaveCount(1)
->and($createdUser->referralCode)->not->toBeNull();
});
it('creates user with inactive status and sets resign date', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->create();
$role = Role::create(['name' => 'Admin']);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
mountCreateComponent($this->user)
->set('form.full_name', 'Jane Doe')
->set('form.username', 'janedoe')
->set('form.email', 'jane@example.com')
->set('form.phone_number', '0812 3456 7891')
->set('form.base_salary', '4.500.000')
->set('form.birthdate', $birthdate)
->set('form.hire_date', $hireDate)
->set('form.gender', Gender::FEMALE->value)
->set('form.status', UserStatus::INACTIVE->value)
->set('form.outlet_ids', [$outlet->id])
->set('form.role_ids', [$role->id])
->call('save')
->assertHasNoErrors();
$createdUser = User::where('email', 'jane@example.com')->first();
$employee = $createdUser->employee;
expect($createdUser->status)->toBe(UserStatus::INACTIVE);
expect($employee->resign_date)->not->toBeNull();
});
it('creates user with multiple outlets', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
$outlet1 = Outlet::factory()->create();
$outlet2 = Outlet::factory()->create();
$outlet3 = Outlet::factory()->create();
$role = Role::create(['name' => 'Admin']);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
mountCreateComponent($this->user)
->set('form.full_name', 'Multiple Outlet User')
->set('form.username', 'multioutlet')
->set('form.email', 'multi@example.com')
->set('form.phone_number', '0812 3456 7892')
->set('form.base_salary', '5.500.000')
->set('form.birthdate', $birthdate)
->set('form.hire_date', $hireDate)
->set('form.gender', Gender::MALE->value)
->set('form.status', UserStatus::ACTIVE->value)
->set('form.outlet_ids', [$outlet1->id, $outlet2->id, $outlet3->id])
->set('form.role_ids', [$role->id])
->call('save')
->assertHasNoErrors();
$createdUser = User::where('email', 'multi@example.com')->first();
expect($createdUser->outlets->count())->toBe(3);
expect($createdUser->outlets->pluck('id')->toArray())->toContain($outlet1->id, $outlet2->id, $outlet3->id);
});
it('creates user with multiple roles', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->create();
$adminRole = Role::create(['name' => 'Admin']);
$leaderRole = Role::create(['name' => 'Leader']);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
mountCreateComponent($this->user)
->set('form.full_name', 'Multiple Role User')
->set('form.username', 'multirole')
->set('form.email', 'role@example.com')
->set('form.phone_number', '0812 3456 7893')
->set('form.base_salary', '6.000.000')
->set('form.birthdate', $birthdate)
->set('form.hire_date', $hireDate)
->set('form.gender', Gender::MALE->value)
->set('form.status', UserStatus::ACTIVE->value)
->set('form.outlet_ids', [$outlet->id])
->set('form.role_ids', [$adminRole->id, $leaderRole->id])
->call('save')
->assertHasNoErrors();
$createdUser = User::where('email', 'role@example.com')->first();
expect($createdUser->roles->count())->toBe(2);
expect($createdUser->roles->pluck('id')->toArray())->toContain($adminRole->id, $leaderRole->id);
});
it('validates required fields', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
$component = mountCreateComponent($this->user);
// Don't set any required fields to ensure they're validated
$component->call('save')
->assertHasErrors([
'form.full_name',
'form.username',
'form.email',
'form.phone_number',
'form.base_salary',
'form.birthdate',
'form.hire_date',
'form.gender',
'form.outlet_ids',
'form.role_ids',
]);
});
it('validates full_name field max length', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->set('form.full_name', str_repeat('a', 101))
->call('save')
->assertHasErrors(['form.full_name']);
});
it('validates username field required', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->set('form.full_name', 'Test User')
->call('save')
->assertHasErrors(['form.username']);
});
it('validates username field alpha numeric', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->set('form.username', 'test-user@123')
->call('save')
->assertHasErrors(['form.username']);
});
it('validates username field min length', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->set('form.username', 'test')
->call('save')
->assertHasErrors(['form.username']);
});
it('validates username field max length', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->set('form.username', str_repeat('a', 21))
->call('save')
->assertHasErrors(['form.username']);
});
it('validates username field uniqueness', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
$existingUser = User::factory()->create(['username' => 'existinguser']);
mountCreateComponent($this->user)
->set('form.username', 'existinguser')
->call('save')
->assertHasErrors(['form.username']);
});
it('validates email field required', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->set('form.full_name', 'Test User')
->set('form.username', 'testuser')
->call('save')
->assertHasErrors(['form.email']);
});
it('validates email field format', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->set('form.email', 'invalid-email')
->call('save')
->assertHasErrors(['form.email']);
});
it('validates email field max length', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->set('form.email', str_repeat('a', 90).'@example.com')
->call('save')
->assertHasErrors(['form.email']);
});
it('validates email field uniqueness', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
$existingUser = User::factory()->create(['email' => 'existing@example.com']);
mountCreateComponent($this->user)
->set('form.email', 'existing@example.com')
->call('save')
->assertHasErrors(['form.email']);
});
it('validates phone_number field required', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->set('form.full_name', 'Test User')
->set('form.username', 'testuser')
->set('form.email', 'test@example.com')
->call('save')
->assertHasErrors(['form.phone_number']);
});
it('validates base_salary field required', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->set('form.full_name', 'Test User')
->set('form.username', 'testuser')
->set('form.email', 'test@example.com')
->call('save')
->assertHasErrors(['form.base_salary']);
});
it('validates birthdate field required', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->set('form.full_name', 'Test User')
->set('form.username', 'testuser')
->set('form.email', 'test@example.com')
->call('save')
->assertHasErrors(['form.birthdate']);
});
it('validates birthdate must be before 18 years ago', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
$birthdate = now()->subYears(17)->format('Y-m-d');
mountCreateComponent($this->user)
->set('form.birthdate', $birthdate)
->call('save')
->assertHasErrors(['form.birthdate']);
});
it('validates hire_date field required', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->set('form.full_name', 'Test User')
->set('form.username', 'testuser')
->set('form.email', 'test@example.com')
->call('save')
->assertHasErrors(['form.hire_date']);
});
it('validates gender field required', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->set('form.full_name', 'Test User')
->set('form.username', 'testuser')
->set('form.email', 'test@example.com')
->call('save')
->assertHasErrors(['form.gender']);
});
it('validates gender field must be valid enum value', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->set('form.gender', 999) // Invalid enum value
->call('save')
->assertHasErrors(['form.gender']);
});
it('validates status field required', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->set('form.full_name', 'Test User')
->set('form.username', 'testuser')
->set('form.email', 'test@example.com')
->set('form.status', 99) // Invalid enum value
->call('save')
->assertHasErrors(['form.status']);
});
it('validates status field must be valid enum value', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->set('form.status', '99')
->call('save')
->assertHasErrors(['form.status']);
});
it('validates outlet_ids field required', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->set('form.full_name', 'Test User')
->set('form.username', 'testuser')
->set('form.email', 'test@example.com')
->call('save')
->assertHasErrors(['form.outlet_ids']);
});
it('validates outlet_ids must be array', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
$this->expectException(TypeError::class);
mountCreateComponent($this->user)
->set('form.outlet_ids', 'not-array')
->call('save')
->assertHasErrors(['form.outlet_ids']);
});
it('validates outlet_ids minimum count', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->set('form.outlet_ids', [])
->call('save')
->assertHasErrors(['form.outlet_ids']);
});
it('validates outlet_ids must exist', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->set('form.outlet_ids', [99999])
->call('save')
->assertHasErrors(['form.outlet_ids.0']);
});
it('validates role_ids field required', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->set('form.full_name', 'Test User')
->set('form.username', 'testuser')
->set('form.email', 'test@example.com')
->call('save')
->assertHasErrors(['form.role_ids']);
});
it('validates role_ids must be array', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
$this->expectException(TypeError::class);
mountCreateComponent($this->user)
->set('form.role_ids', 'not-array')
->call('save')
->assertHasErrors(['form.role_ids']);
});
it('validates role_ids minimum count', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->set('form.role_ids', [])
->call('save')
->assertHasErrors(['form.role_ids']);
});
it('validates role_ids must exist', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
mountCreateComponent($this->user)
->set('form.role_ids', [99999])
->call('save')
->assertHasErrors(['form.role_ids.0']);
});
it('creates employee code automatically', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->create();
$role = Role::create(['name' => 'Admin']);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
mountCreateComponent($this->user)
->set('form.full_name', 'Employee Code Test')
->set('form.username', 'empcodetest')
->set('form.email', 'empcode@example.com')
->set('form.phone_number', '0812 3456 7894')
->set('form.base_salary', '4.000.000')
->set('form.birthdate', $birthdate)
->set('form.hire_date', $hireDate)
->set('form.gender', Gender::MALE->value)
->set('form.status', UserStatus::ACTIVE->value)
->set('form.outlet_ids', [$outlet->id])
->set('form.role_ids', [$role->id])
->call('save')
->assertHasNoErrors();
$createdUser = User::where('email', 'empcode@example.com')->first();
$employee = $createdUser->employee;
expect($employee->code)->toBeString();
expect($employee->code)->toStartWith('EMP');
});
it('creates referral code automatically', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->create();
$role = Role::create(['name' => 'Admin']);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
mountCreateComponent($this->user)
->set('form.full_name', 'Referral Code Test')
->set('form.username', 'refcodetest')
->set('form.email', 'refcode@example.com')
->set('form.phone_number', '0812 3456 7895')
->set('form.base_salary', '4.500.000')
->set('form.birthdate', $birthdate)
->set('form.hire_date', $hireDate)
->set('form.gender', Gender::MALE->value)
->set('form.status', UserStatus::ACTIVE->value)
->set('form.outlet_ids', [$outlet->id])
->set('form.role_ids', [$role->id])
->call('save')
->assertHasNoErrors();
$createdUser = User::where('email', 'refcode@example.com')->first();
expect($createdUser->referralCode)->not->toBeNull();
expect($createdUser->referralCode->code)->toBeString();
});
it('formats base salary correctly when storing', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->create();
$role = Role::create(['name' => 'Admin']);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
mountCreateComponent($this->user)
->set('form.full_name', 'Salary Format Test')
->set('form.username', 'salarytest')
->set('form.email', 'salary@example.com')
->set('form.phone_number', '0812 3456 7896')
->set('form.base_salary', '5.500.000')
->set('form.birthdate', $birthdate)
->set('form.hire_date', $hireDate)
->set('form.gender', Gender::MALE->value)
->set('form.status', UserStatus::ACTIVE->value)
->set('form.outlet_ids', [$outlet->id])
->set('form.role_ids', [$role->id])
->call('save')
->assertHasNoErrors();
$createdUser = User::where('email', 'salary@example.com')->first();
$employee = $createdUser->employee;
expect($employee->base_salary)->toBe(5500000);
});
it('allows nullable address field', function () {
$permission = Permission::create(['name' => 'create user']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->create();
$role = Role::create(['name' => 'Admin']);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
mountCreateComponent($this->user)
->set('form.full_name', 'No Address Test')
->set('form.username', 'noaddresstest')
->set('form.email', 'noaddress@example.com')
->set('form.phone_number', '0812 3456 7897')
->set('form.base_salary', '4.000.000')
->set('form.birthdate', $birthdate)
->set('form.hire_date', $hireDate)
->set('form.address', null)
->set('form.gender', Gender::MALE->value)
->set('form.status', UserStatus::ACTIVE->value)
->set('form.outlet_ids', [$outlet->id])
->set('form.role_ids', [$role->id])
->call('save')
->assertHasNoErrors();
$createdUser = User::where('email', 'noaddress@example.com')->first();
$employee = $createdUser->employee;
expect($employee->address)->toBeNull();
});

View File

@ -1,677 +0,0 @@
<?php
use App\Enums\Gender;
use App\Enums\UserStatus;
use App\Livewire\Studio\Master\User\Edit;
use App\Livewire\Studio\Master\User\Index;
use App\Models\Employee;
use App\Models\Outlet;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
use Livewire\Features\SupportTesting\Testable;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->user = User::factory()
->active()
->has(Employee::factory())
->create();
});
function createEditUser(array $attributes = []): User
{
return User::factory()
->active()
->has(Employee::factory())
->state(fn () => array_merge([
'status' => fake()->randomElement(UserStatus::cases()),
], $attributes))
->create();
}
function mountEditComponent(User $user, User $employeeUser): Testable
{
return Livewire::actingAs($user)->test(Edit::class, ['user' => $employeeUser]);
}
it('renders page successfully', function () {
$employeeUser = createEditUser();
\Spatie\Permission\Models\Role::firstOrCreate(['name' => 'Admin']);
$employeeUser->assignRole('Admin');
mountEditComponent($this->user, $employeeUser)
->assertViewIs('livewire.studio.master.user.form')
->assertViewHas('pageTitle', 'Ubah Pegawai');
});
it('loads outlets and roles on mount', function () {
$employeeUser = createEditUser();
$outlet = Outlet::factory()->create();
$role = Role::create(['name' => 'Admin']);
$component = mountEditComponent($this->user, $employeeUser);
expect($component->outlets)->toBeArray();
expect($component->outlets)->toHaveKey($outlet->id);
expect($component->roles)->toBeArray();
expect($component->roles)->toHaveKey($role->id);
});
it('loads existing user data into form', function () {
$outlet = Outlet::factory()->create();
$role = Role::create(['name' => 'Admin']);
$employeeUser = createEditUser([
'username' => 'existinguser',
'email' => 'existing@example.com',
'status' => UserStatus::ACTIVE,
]);
$employeeUser->employee->update([
'full_name' => 'Existing User',
'phone_number' => '081234567890',
'base_salary' => 5000000,
'birthdate' => '1995-01-15',
'hire_date' => '2020-01-01',
'address' => 'Existing Address',
'gender' => Gender::MALE,
]);
$employeeUser->assignRole($role->id);
$employeeUser->outlets()->attach($outlet->id);
$component = mountEditComponent($this->user, $employeeUser);
expect($component->form->full_name)->toBe('Existing User')
->and($component->form->username)->toBe('existinguser')
->and($component->form->email)->toBe('existing@example.com')
->and($component->form->phone_number)->toBe('081234567890')
->and($component->form->base_salary)->toBe('5000000');
expect($component->form->birthdate)->toBe('1995-01-15');
expect($component->form->hire_date)->toBe('2020-01-01');
expect($component->form->address)->toBe('Existing Address');
expect((int) $component->form->gender)->toBe(Gender::MALE->value);
expect((int) $component->form->status)->toBe(UserStatus::ACTIVE->value);
expect($component->form->outlet_ids)->toContain($outlet->id);
expect($component->form->role_ids)->toContain($role->id);
});
it('excludes developer role from roles list', function () {
Role::firstOrCreate(['name' => 'Admin']);
Role::firstOrCreate(['name' => 'Developer']);
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditUser();
$employeeUser->assignRole($role->id);
$component = mountEditComponent($this->user, $employeeUser);
$developerRole = Role::where('name', 'Developer')->first();
expect($component->roles)->not->toHaveKey($developerRole->id);
});
it('prevents update when user is unauthorized', function () {
Role::firstOrCreate(['name' => 'Admin']);
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditUser();
$employeeUser->assignRole($role->id);
Gate::define('update user', fn () => false);
mountEditComponent($this->user, $employeeUser)
->call('save')
->assertForbidden();
});
it('updates user successfully when authorized', function () {
$permission = Permission::create(['name' => 'update user']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->create();
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditUser();
$employeeUser->assignRole($role->id);
$employeeUser->outlets()->attach($outlet->id);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
mountEditComponent($this->user, $employeeUser)
->set('form.full_name', 'Updated Name')
->set('form.username', 'updateduser')
->set('form.email', 'updated@example.com')
->set('form.phone_number', '0813 9999 8888')
->set('form.base_salary', '6.000.000')
->set('form.birthdate', $birthdate)
->set('form.hire_date', $hireDate)
->set('form.address', 'Updated Address')
->set('form.gender', Gender::FEMALE->value)
->set('form.status', UserStatus::ACTIVE->value)
->set('form.outlet_ids', [$outlet->id])
->set('form.role_ids', [$role->id])
->call('save')
->assertHasNoErrors()
->assertRedirect(Index::class);
$employeeUser->refresh();
$employeeUser->load(['employee', 'outlets', 'roles']);
expect($employeeUser->username)->toBe('updateduser');
expect($employeeUser->email)->toBe('updated@example.com');
expect($employeeUser->status)->toBe(UserStatus::ACTIVE);
expect($employeeUser->employee->full_name)->toBe('Updated Name');
expect($employeeUser->employee->phone_number)->toBe('0813 9999 8888');
expect($employeeUser->employee->base_salary)->toBe(6000000);
expect($employeeUser->employee->address)->toBe('Updated Address');
expect($employeeUser->employee->gender)->toBe(Gender::FEMALE);
expect($employeeUser->outlets->pluck('id')->toArray())->toContain($outlet->id);
expect($employeeUser->roles->pluck('id')->toArray())->toContain($role->id);
});
it('updates user with inactive status and sets resign date', function () {
$permission = Permission::create(['name' => 'update user']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->create();
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditUser(['status' => UserStatus::ACTIVE]);
$employeeUser->assignRole($role->id);
$employeeUser->outlets()->attach($outlet->id);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
mountEditComponent($this->user, $employeeUser)
->set('form.status', UserStatus::INACTIVE->value)
->set('form.full_name', $employeeUser->employee->full_name)
->set('form.username', $employeeUser->username)
->set('form.email', $employeeUser->email)
->set('form.phone_number', $employeeUser->employee->phone_number)
->set('form.base_salary', '5.000.000')
->set('form.birthdate', $birthdate)
->set('form.hire_date', $hireDate)
->set('form.gender', $employeeUser->employee->gender->value)
->set('form.outlet_ids', [$outlet->id])
->set('form.role_ids', [$role->id])
->call('save')
->assertHasNoErrors();
$employeeUser->refresh();
$employeeUser->load('employee');
expect($employeeUser->status)->toBe(UserStatus::INACTIVE);
expect($employeeUser->employee->resign_date)->not->toBeNull();
});
it('clears resign date when status changes to active', function () {
$permission = Permission::create(['name' => 'update user']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->create();
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditUser([
'status' => UserStatus::INACTIVE,
]);
$employeeUser->assignRole($role->id);
$employeeUser->outlets()->attach($outlet->id);
$employee = $employeeUser->employee;
$employee->update(['resign_date' => now()->subDays(5)]);
expect($employee->resign_date)->not->toBeNull();
$birthdate = $employee->birthdate;
$hireDate = $employee->hire_date;
mountEditComponent($this->user, $employeeUser)
->set('form.status', UserStatus::ACTIVE->value)
->set('form.full_name', $employee->full_name)
->set('form.username', $employeeUser->username)
->set('form.email', $employeeUser->email)
->set('form.phone_number', $employee->phone_number)
->set('form.base_salary', '5.000.000')
->set('form.birthdate', $birthdate)
->set('form.hire_date', $hireDate)
->set('form.gender', $employee->gender->value)
->set('form.outlet_ids', [$outlet->id])
->set('form.role_ids', [$role->id])
->call('save')
->assertHasNoErrors();
$employeeUser->refresh();
expect($employeeUser->status)->toBe(UserStatus::ACTIVE);
expect($employee->resign_date)->not->toBeNull();
});
it('updates user with multiple outlets', function () {
$permission = Permission::create(['name' => 'update user']);
$this->user->givePermissionTo($permission);
$outlet1 = Outlet::factory()->create();
$outlet2 = Outlet::factory()->create();
$outlet3 = Outlet::factory()->create();
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditUser();
$employeeUser->assignRole($role->id);
$employeeUser->outlets()->attach($outlet1->id);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
mountEditComponent($this->user, $employeeUser)
->set('form.full_name', $employeeUser->employee->full_name)
->set('form.username', $employeeUser->username)
->set('form.email', $employeeUser->email)
->set('form.phone_number', $employeeUser->employee->phone_number)
->set('form.base_salary', '5.000.000')
->set('form.birthdate', $birthdate)
->set('form.hire_date', $hireDate)
->set('form.gender', $employeeUser->employee->gender->value)
->set('form.status', UserStatus::ACTIVE->value)
->set('form.outlet_ids', [$outlet1->id, $outlet2->id, $outlet3->id])
->set('form.role_ids', [$role->id])
->call('save')
->assertHasNoErrors();
$employeeUser->refresh();
$employeeUser->load('outlets');
expect($employeeUser->outlets->count())->toBe(3);
expect($employeeUser->outlets->pluck('id')->toArray())->toContain($outlet1->id, $outlet2->id, $outlet3->id);
});
it('updates user with multiple roles', function () {
$permission = Permission::create(['name' => 'update user']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->create();
$adminRole = Role::firstOrCreate(['name' => 'Admin']);
$leaderRole = Role::firstOrCreate(['name' => 'Leader']);
$employeeUser = createEditUser();
$employeeUser->assignRole($adminRole->id);
$employeeUser->outlets()->attach($outlet->id);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
mountEditComponent($this->user, $employeeUser)
->set('form.full_name', $employeeUser->employee->full_name)
->set('form.username', $employeeUser->username)
->set('form.email', $employeeUser->email)
->set('form.phone_number', $employeeUser->employee->phone_number)
->set('form.base_salary', '5.000.000')
->set('form.birthdate', $birthdate)
->set('form.hire_date', $hireDate)
->set('form.gender', $employeeUser->employee->gender->value)
->set('form.status', UserStatus::ACTIVE->value)
->set('form.outlet_ids', [$outlet->id])
->set('form.role_ids', [$adminRole->id, $leaderRole->id])
->call('save')
->assertHasNoErrors();
$employeeUser->refresh();
$employeeUser->load('roles');
expect($employeeUser->roles->count())->toBe(2);
expect($employeeUser->roles->pluck('id')->toArray())->toContain($adminRole->id, $leaderRole->id);
});
it('syncs outlets correctly when updating', function () {
$permission = Permission::create(['name' => 'update user']);
$this->user->givePermissionTo($permission);
$outlet1 = Outlet::factory()->create();
$outlet2 = Outlet::factory()->create();
$outlet3 = Outlet::factory()->create();
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditUser();
$employeeUser->assignRole($role->id);
$employeeUser->outlets()->attach([$outlet1->id, $outlet2->id]);
expect($employeeUser->outlets->count())->toBe(2);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
mountEditComponent($this->user, $employeeUser)
->set('form.full_name', $employeeUser->employee->full_name)
->set('form.username', $employeeUser->username)
->set('form.email', $employeeUser->email)
->set('form.phone_number', $employeeUser->employee->phone_number)
->set('form.base_salary', '5.000.000')
->set('form.birthdate', $birthdate)
->set('form.hire_date', $hireDate)
->set('form.gender', $employeeUser->employee->gender->value)
->set('form.status', UserStatus::ACTIVE->value)
->set('form.outlet_ids', [$outlet2->id, $outlet3->id])
->set('form.role_ids', [$role->id])
->call('save')
->assertHasNoErrors();
$employeeUser->refresh();
$employeeUser->load('outlets');
expect($employeeUser->outlets->count())->toBe(2);
expect($employeeUser->outlets->pluck('id')->toArray())->toContain($outlet2->id, $outlet3->id);
expect($employeeUser->outlets->pluck('id')->toArray())->not->toContain($outlet1->id);
});
it('syncs roles correctly when updating', function () {
$permission = Permission::create(['name' => 'update user']);
$this->user->givePermissionTo($permission);
$outlet = Outlet::factory()->create();
$adminRole = Role::firstOrCreate(['name' => 'Admin']);
$leaderRole = Role::firstOrCreate(['name' => 'Leader']);
$ownerRole = Role::firstOrCreate(['name' => 'Owner']);
$employeeUser = createEditUser();
$employeeUser->assignRole([$adminRole->id, $leaderRole->id]);
$employeeUser->outlets()->attach($outlet->id);
expect($employeeUser->roles->count())->toBe(2);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
mountEditComponent($this->user, $employeeUser)
->set('form.full_name', $employeeUser->employee->full_name)
->set('form.username', $employeeUser->username)
->set('form.email', $employeeUser->email)
->set('form.phone_number', $employeeUser->employee->phone_number)
->set('form.base_salary', '5.000.000')
->set('form.birthdate', $birthdate)
->set('form.hire_date', $hireDate)
->set('form.gender', $employeeUser->employee->gender->value)
->set('form.status', UserStatus::ACTIVE->value)
->set('form.outlet_ids', [$outlet->id])
->set('form.role_ids', [$leaderRole->id, $ownerRole->id])
->call('save')
->assertHasNoErrors();
$employeeUser->refresh();
$employeeUser->load('roles');
expect($employeeUser->roles->count())->toBe(2);
expect($employeeUser->roles->pluck('id')->toArray())->toContain($leaderRole->id, $ownerRole->id);
expect($employeeUser->roles->pluck('id')->toArray())->not->toContain($adminRole->id);
});
it('validates required fields', function () {
$permission = Permission::create(['name' => 'update user']);
$this->user->givePermissionTo($permission);
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditUser();
$employeeUser->assignRole($role->id);
$component = mountEditComponent($this->user, $employeeUser);
// Clear all fields
$component->set('form.full_name', '')
->set('form.username', '')
->set('form.email', '')
->set('form.phone_number', '')
->set('form.base_salary', '')
->set('form.birthdate', '')
->set('form.hire_date', '')
->set('form.gender', '')
->set('form.outlet_ids', [])
->set('form.role_ids', [])
->call('save')
->assertHasErrors([
'form.full_name',
'form.username',
'form.email',
'form.phone_number',
'form.base_salary',
'form.birthdate',
'form.hire_date',
'form.gender',
'form.outlet_ids',
'form.role_ids',
]);
});
it('validates username uniqueness ignores current user', function () {
$permission = Permission::create(['name' => 'update user']);
$this->user->givePermissionTo($permission);
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditUser(['username' => 'existinguser']);
$employeeUser->assignRole($role->id);
$outlet = Outlet::factory()->create();
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
// Should allow keeping the same username
mountEditComponent($this->user, $employeeUser)
->set('form.full_name', $employeeUser->employee->full_name)
->set('form.username', 'existinguser') // Same username
->set('form.email', $employeeUser->email)
->set('form.phone_number', $employeeUser->employee->phone_number)
->set('form.base_salary', '5.000.000')
->set('form.birthdate', $birthdate)
->set('form.hire_date', $hireDate)
->set('form.gender', $employeeUser->employee->gender->value)
->set('form.status', UserStatus::ACTIVE->value)
->set('form.outlet_ids', [$outlet->id])
->set('form.role_ids', [$role->id])
->call('save')
->assertHasNoErrors(['form.username']);
});
it('validates username uniqueness prevents duplicate', function () {
$permission = Permission::create(['name' => 'update user']);
$this->user->givePermissionTo($permission);
$role = Role::firstOrCreate(['name' => 'Admin']);
$existingUser = createEditUser(['username' => 'existinguser']);
$existingUser->assignRole($role->id);
$employeeUser = createEditUser(['username' => 'otheruser']);
$employeeUser->assignRole($role->id);
$outlet = Outlet::factory()->create();
$role = Role::firstOrCreate(['name' => 'Admin']);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
mountEditComponent($this->user, $employeeUser)
->set('form.username', 'existinguser') // Duplicate username
->set('form.full_name', $employeeUser->employee->full_name)
->set('form.email', $employeeUser->email)
->set('form.phone_number', $employeeUser->employee->phone_number)
->set('form.base_salary', '5.000.000')
->set('form.birthdate', $birthdate)
->set('form.hire_date', $hireDate)
->set('form.gender', $employeeUser->employee->gender->value)
->set('form.status', UserStatus::ACTIVE->value)
->set('form.outlet_ids', [$outlet->id])
->set('form.role_ids', [$role->id])
->call('save')
->assertHasErrors(['form.username']);
});
it('validates email uniqueness ignores current user', function () {
$permission = Permission::create(['name' => 'update user']);
$this->user->givePermissionTo($permission);
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditUser(['email' => 'existing@example.com']);
$employeeUser->assignRole($role->id);
$outlet = Outlet::factory()->create();
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
// Should allow keeping the same email
mountEditComponent($this->user, $employeeUser)
->set('form.full_name', $employeeUser->employee->full_name)
->set('form.username', $employeeUser->username)
->set('form.email', 'existing@example.com') // Same email
->set('form.phone_number', $employeeUser->employee->phone_number)
->set('form.base_salary', '5.000.000')
->set('form.birthdate', $birthdate)
->set('form.hire_date', $hireDate)
->set('form.gender', $employeeUser->employee->gender->value)
->set('form.status', UserStatus::ACTIVE->value)
->set('form.outlet_ids', [$outlet->id])
->set('form.role_ids', [$role->id])
->call('save')
->assertHasNoErrors(['form.email']);
});
it('validates email uniqueness prevents duplicate', function () {
$permission = Permission::create(['name' => 'update user']);
$this->user->givePermissionTo($permission);
$role = Role::firstOrCreate(['name' => 'Admin']);
$existingUser = createEditUser(['email' => 'existing@example.com']);
$existingUser->assignRole($role->id);
$employeeUser = createEditUser(['email' => 'other@example.com']);
$employeeUser->assignRole($role->id);
$outlet = Outlet::factory()->create();
$role = Role::firstOrCreate(['name' => 'Admin']);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
mountEditComponent($this->user, $employeeUser)
->set('form.email', 'existing@example.com') // Duplicate email
->set('form.full_name', $employeeUser->employee->full_name)
->set('form.username', $employeeUser->username)
->set('form.phone_number', $employeeUser->employee->phone_number)
->set('form.base_salary', '5.000.000')
->set('form.birthdate', $birthdate)
->set('form.hire_date', $hireDate)
->set('form.gender', $employeeUser->employee->gender->value)
->set('form.status', UserStatus::ACTIVE->value)
->set('form.outlet_ids', [$outlet->id])
->set('form.role_ids', [$role->id])
->call('save')
->assertHasErrors(['form.email']);
});
it('validates birthdate must be before 18 years ago', function () {
$permission = Permission::create(['name' => 'update user']);
$this->user->givePermissionTo($permission);
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditUser();
$employeeUser->assignRole($role->id);
$outlet = Outlet::factory()->create();
$role = Role::firstOrCreate(['name' => 'Admin']);
$birthdate = now()->subYears(17)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
mountEditComponent($this->user, $employeeUser)
->set('form.birthdate', $birthdate)
->set('form.full_name', $employeeUser->employee->full_name)
->set('form.username', $employeeUser->username)
->set('form.email', $employeeUser->email)
->set('form.phone_number', $employeeUser->employee->phone_number)
->set('form.base_salary', '5.000.000')
->set('form.hire_date', $hireDate)
->set('form.gender', $employeeUser->employee->gender->value)
->set('form.status', UserStatus::ACTIVE->value)
->set('form.outlet_ids', [$outlet->id])
->set('form.role_ids', [$role->id])
->call('save')
->assertHasErrors(['form.birthdate']);
});
it('formats base salary correctly when updating', function () {
$permission = Permission::create(['name' => 'update user']);
$this->user->givePermissionTo($permission);
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditUser();
$employeeUser->assignRole($role->id);
$outlet = Outlet::factory()->create();
$role = Role::firstOrCreate(['name' => 'Admin']);
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
mountEditComponent($this->user, $employeeUser)
->set('form.base_salary', '7.500.000')
->set('form.full_name', $employeeUser->employee->full_name)
->set('form.username', $employeeUser->username)
->set('form.email', $employeeUser->email)
->set('form.phone_number', $employeeUser->employee->phone_number)
->set('form.birthdate', $birthdate)
->set('form.hire_date', $hireDate)
->set('form.gender', $employeeUser->employee->gender->value)
->set('form.status', UserStatus::ACTIVE->value)
->set('form.outlet_ids', [$outlet->id])
->set('form.role_ids', [$role->id])
->call('save')
->assertHasNoErrors();
$employeeUser->refresh();
$employeeUser->load('employee');
expect($employeeUser->employee->base_salary)->toBe(7500000);
});
it('allows nullable address field when updating', function () {
$permission = Permission::create(['name' => 'update user']);
$this->user->givePermissionTo($permission);
$role = Role::firstOrCreate(['name' => 'Admin']);
$employeeUser = createEditUser();
$employeeUser->employee->update(['address' => 'Old Address']);
$employeeUser->assignRole($role->id);
$outlet = Outlet::factory()->create();
$birthdate = now()->subYears(25)->format('Y-m-d');
$hireDate = now()->subMonths(6)->format('Y-m-d');
mountEditComponent($this->user, $employeeUser)
->set('form.address', null)
->set('form.full_name', $employeeUser->employee->full_name)
->set('form.username', $employeeUser->username)
->set('form.email', $employeeUser->email)
->set('form.phone_number', $employeeUser->employee->phone_number)
->set('form.base_salary', '5.000.000')
->set('form.birthdate', $birthdate)
->set('form.hire_date', $hireDate)
->set('form.gender', $employeeUser->employee->gender->value)
->set('form.status', UserStatus::ACTIVE->value)
->set('form.outlet_ids', [$outlet->id])
->set('form.role_ids', [$role->id])
->call('save')
->assertHasNoErrors();
$employeeUser->refresh();
$employeeUser->load('employee');
expect($employeeUser->employee->address)->toBeNull();
});

View File

@ -1,229 +0,0 @@
<?php
use App\Enums\UserStatus;
use App\Livewire\Studio\Master\User\Index;
use App\Models\Employee;
use App\Models\Outlet;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Features\SupportTesting\Testable;
use Livewire\Livewire;
uses(RefreshDatabase::class);
beforeEach(function () {
// Create roles for testing
collect(['Developer', 'Owner', 'Leader', 'Admin', 'Partner', 'Customer'])
->each(fn ($role) => \Spatie\Permission\Models\Role::create(['name' => $role]));
$this->user = User::factory()
->active()
->has(Employee::factory())
->create();
$this->user->assignRole('Admin'); // Give admin role to test user
// Give necessary permissions for testing (since Admin role in seeder doesn't have these permissions)
\Spatie\Permission\Models\Permission::create(['name' => 'delete user']);
\Spatie\Permission\Models\Permission::create(['name' => 'reset password user']);
$this->user->givePermissionTo(['delete user', 'reset password user']);
});
function createUser(array $attributes = []): User
{
return User::factory()
->active()
->has(Employee::factory())
->state(fn () => array_merge([
'status' => fake()->randomElement(UserStatus::cases()),
], $attributes))
->create();
}
function mountIndexComponent(User $user): Testable
{
return Livewire::actingAs($user)->test(Index::class);
}
it('renders page successfully', function () {
mountIndexComponent($this->user)
->assertViewIs('livewire.studio.master.user.index')
->assertViewHas('pageTitle', 'Pegawai');
});
it('loads employees as Eloquent collection', function () {
$user = createUser();
$component = mountIndexComponent($this->user);
expect(
$component->get('employees')->pluck('id')
)->toContain($user->id);
});
it('loads employees with relationships', function () {
$user = createUser();
$user->assignRole('Admin'); // Assign role so user appears in list
$outlet = Outlet::factory()->create();
$user->outlets()->attach($outlet->id);
$component = mountIndexComponent($this->user);
$loadedEmployee = $component->get('employees')->first(fn ($emp) => $emp->user_id === $user->id);
expect($loadedEmployee)->not->toBeNull()
->and($loadedEmployee->relationLoaded('user'))->toBeTrue()
->and($loadedEmployee->user->relationLoaded('outlets'))->toBeTrue()
->and($loadedEmployee->user->outlets)->toHaveCount(1);
});
it('excludes developer users from listing', function () {
$developer = createUser();
$developer->assignRole('Developer');
$admin = createUser();
$admin->assignRole('Admin');
$component = mountIndexComponent($this->user);
$employeeNames = $component->get('employees')->pluck('full_name')->toArray();
expect($employeeNames)->not->toContain($developer->employee->full_name);
expect($employeeNames)->toContain($admin->employee->full_name);
});
it('displays all employees', function () {
$initialCount = Employee::count();
for ($i = 0; $i < 5; $i++) {
createUser();
}
$component = mountIndexComponent($this->user);
expect($component->get('employees'))->toHaveCount($initialCount + 5);
expect($component->get('employees')->first())->toBeInstanceOf(Employee::class);
});
it('filters employees by search query', function () {
$user1 = createUser(['username' => 'john_doe']);
$user2 = createUser(['username' => 'jane_smith']);
$component = mountIndexComponent($this->user);
expect(count($component->get('employees')))->toBeGreaterThanOrEqual(2); // At least 2 new users
// Test filtering by username
$filteredComponent = mountIndexComponent($this->user)->set('search', 'john_doe');
expect($filteredComponent->get('employees'))->toHaveCount(1);
expect($filteredComponent->get('employees')->first()->user->username)->toBe('john_doe');
});
it('filters employees by status', function () {
$activeUser = createUser(['status' => UserStatus::ACTIVE]);
$inactiveUser = createUser(['status' => UserStatus::INACTIVE]);
$component = mountIndexComponent($this->user);
expect(count($component->get('employees')))->toBeGreaterThanOrEqual(2); // At least 2 new users
$filteredComponent = mountIndexComponent($this->user)->set('status', [UserStatus::ACTIVE->value]);
expect(count($filteredComponent->get('employees')))->toBeGreaterThanOrEqual(1); // At least the active user
expect($filteredComponent->get('employees')->contains(fn ($emp) => $emp->user->status->value === UserStatus::ACTIVE->value))->toBeTrue();
});
it('filters employees by multiple statuses', function () {
$activeUser = createUser(['status' => UserStatus::ACTIVE]);
$inactiveUser = createUser(['status' => UserStatus::INACTIVE]);
$component = mountIndexComponent($this->user);
expect(count($component->get('employees')))->toBeGreaterThanOrEqual(2); // At least 2 new users
$filteredComponent = mountIndexComponent($this->user)->set('status', [
UserStatus::ACTIVE->value,
UserStatus::INACTIVE->value,
]);
expect(count($filteredComponent->get('employees')))->toBeGreaterThan(2); // All users with these statuses
});
it('loads employees ordered by latest', function () {
$user1 = createUser();
sleep(1);
$user2 = createUser();
$component = mountIndexComponent($this->user);
expect($component->get('employees')->first()->id)->toBe($user2->id);
expect($component->get('employees')->last()->id)->toBe($user1->id);
});
it('loads employees with all required relationships', function () {
$user = createUser();
$user->assignRole('Admin'); // Ensure user appears in list
$outlet = Outlet::factory()->create();
$user->outlets()->attach($outlet->id);
$component = mountIndexComponent($this->user);
$loadedEmployee = $component->get('employees')->first(fn ($emp) => $emp->user_id === $user->id);
expect($loadedEmployee)->not->toBeNull()
->and($loadedEmployee->user->outlets)->toHaveCount(1)
->and($loadedEmployee->user->roles)->toHaveCount(1) // Admin role assigned
->and($loadedEmployee->full_name)->toBe($user->employee->full_name);
});
it('deletes an employee successfully', function () {
$user = createUser();
mountIndexComponent($this->user)
->call('delete', $user)
->assertHasNoErrors();
$this->assertSoftDeleted('users', ['id' => $user->id]);
$this->assertSoftDeleted('employees', ['user_id' => $user->id]);
});
it('handles empty search results gracefully', function () {
createUser(['username' => 'existing_user']);
$component = mountIndexComponent($this->user)->set('search', 'NonExistentUser');
expect($component->get('employees'))->toHaveCount(0);
});
it('maintains search state across component lifecycle', function () {
$user1 = createUser(['username' => 'test_user']);
$user2 = createUser(['username' => 'another_user']);
$component = mountIndexComponent($this->user)->set('search', 'test');
expect($component->get('employees'))->toHaveCount(1);
expect($component->get('search'))->toBe('test');
});
it('handles status filter with empty array', function () {
createUser(['status' => UserStatus::ACTIVE]);
createUser(['status' => UserStatus::INACTIVE]);
$component = mountIndexComponent($this->user)->set('status', []);
expect(count($component->get('employees')))->toBeGreaterThanOrEqual(2); // At least 2 new users
});
it('loads employees efficiently with eager loading', function () {
$users = collect();
for ($i = 0; $i < 10; $i++) {
$users->push(createUser());
}
$component = mountIndexComponent($this->user);
// Verify relationships are loaded
$loadedEmployees = $component->get('employees');
foreach ($loadedEmployees as $loadedEmployee) {
expect($loadedEmployee->relationLoaded('user'))->toBeTrue();
expect($loadedEmployee->user->relationLoaded('outlets'))->toBeTrue();
}
});

View File

@ -1,169 +0,0 @@
<?php
use App\Livewire\Studio\Setting\Account;
use App\Models\Employee;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash;
use Livewire\Livewire;
use Spatie\Permission\Models\Permission;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->user = User::factory()->create([
'password' => Hash::make('password'),
]);
$this->employee = Employee::factory()->create([
'user_id' => $this->user->id,
]);
// Setup basic permissions
Permission::create(['name' => 'view account']);
Permission::create(['name' => 'update account']);
Permission::create(['name' => 'update profile']);
Permission::create(['name' => 'update password']);
$this->user->givePermissionTo(['view account', 'update account', 'update profile', 'update password']);
// Share dummy sidebar to avoid undefined variable error
Illuminate\Support\Facades\View::share('sidebar', []);
});
test('can render account settings page', function () {
$this->actingAs($this->user)
->get(route('studio.setting.account'))
->assertOk()
->assertSeeLivewire(Account::class)
->assertSee('Akun');
});
test('mounts with correct user data', function () {
Livewire::actingAs($this->user)
->test(Account::class)
->assertSet('accountForm.username', $this->user->username)
->assertSet('accountForm.email', $this->user->email)
->assertSet('profileForm.full_name', $this->employee->full_name)
->assertSet('profileForm.phone_number', $this->employee->phone_number);
});
test('can update account information', function () {
Livewire::actingAs($this->user)
->test(Account::class)
->set('accountForm.username', 'newusername')
->set('accountForm.email', 'newemail@example.com')
->call('updateAccount')
->assertHasNoErrors();
expect($this->user->fresh())
->username->toBe('newusername')
->email->toBe('newemail@example.com');
});
test('validates account information', function () {
$existingUser = User::factory()->create(['username' => 'takenuser', 'email' => 'taken@example.com']);
Livewire::actingAs($this->user)
->test(Account::class)
->set('accountForm.username', '') // Required
->set('accountForm.email', 'not-an-email') // Email format
->call('updateAccount')
->assertHasErrors(['accountForm.username', 'accountForm.email'])
->set('accountForm.username', 'takenuser') // Unique
->set('accountForm.email', 'taken@example.com') // Unique
->call('updateAccount')
->assertHasErrors(['accountForm.username', 'accountForm.email']);
});
test('can update profile information', function () {
Livewire::actingAs($this->user)
->test(Account::class)
->set('profileForm.full_name', 'New Name')
->set('profileForm.phone_number', '0812 3456 7890')
->call('updateProfile')
->assertHasNoErrors();
expect($this->employee->fresh())
->full_name->toBe('New Name')
->phone_number->toBe('0812 3456 7890');
});
test('validates profile information', function () {
Livewire::actingAs($this->user)
->test(Account::class)
->set('profileForm.full_name', '') // Required
->set('profileForm.phone_number', 'invalid-phone') // Regex/Format
->call('updateProfile')
->assertHasErrors(['profileForm.full_name', 'profileForm.phone_number']);
});
test('can update password', function () {
Livewire::actingAs($this->user)
->test(Account::class)
->set('passwordForm.current_password', 'password')
->set('passwordForm.new_password', 'Sup3rStr0ngP@ssw0rd!')
->set('passwordForm.new_confirm_password', 'Sup3rStr0ngP@ssw0rd!')
->call('updatePassword')
->assertHasNoErrors()
->assertRedirect(route('login'));
// Assert that the user was logged out or session invalidated?
// Hash check is tricky due to re-hashing or instance changes.
// For now, assume if no errors and redirect happened, it worked.
// We can check if "new" password works for login if we really want, but that requires more setup.
});
test('fails to update password with incorrect current password', function () {
Livewire::actingAs($this->user)
->test(Account::class)
->set('passwordForm.current_password', 'wrongpassword')
->set('passwordForm.new_password', 'Sup3rStr0ngP@ssw0rd!')
->set('passwordForm.new_confirm_password', 'Sup3rStr0ngP@ssw0rd!')
->call('updatePassword');
$this->assertTrue(Hash::check('password', $this->user->fresh()->password));
});
test('validates password complexity', function () {
Livewire::actingAs($this->user)
->test(Account::class)
->set('passwordForm.current_password', 'password')
->set('passwordForm.new_password', 'weak')
->set('passwordForm.new_confirm_password', 'weak')
->call('updatePassword')
->assertHasErrors(['passwordForm.new_password']);
});
test('cannot update account without permission', function () {
$this->user->revokePermissionTo('update account');
Livewire::actingAs($this->user)
->test(Account::class)
->set('accountForm.username', 'newusername')
->call('updateAccount')
->assertForbidden();
});
test('cannot update profile without permission', function () {
$this->user->revokePermissionTo('update profile');
Livewire::actingAs($this->user)
->test(Account::class)
->set('profileForm.full_name', 'New Name')
->call('updateProfile')
->assertForbidden();
});
test('cannot update password without permission', function () {
$this->user->revokePermissionTo('update password');
Livewire::actingAs($this->user)
->test(Account::class)
->set('passwordForm.current_password', 'password')
->set('passwordForm.new_password', 'NewPassword123!')
->set('passwordForm.new_confirm_password', 'NewPassword123!')
->call('updatePassword')
->assertForbidden();
});

View File

@ -1,301 +0,0 @@
<?php
use App\Livewire\Studio\Setting\Application;
use App\Models\User;
use App\Settings\ContactSettings;
use App\Settings\GeneralSettings;
use App\Settings\SeoSettings;
use App\Settings\SocialSettings;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
use Spatie\Permission\Models\Role;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->seed(RolePermissionSeeder::class);
$this->user = User::factory()
->active()
->create();
$this->ownerRole = Role::where('name', 'Owner')->first();
$this->user->roles()->attach($this->ownerRole->id);
});
function mountApplicationComponent(User $user)
{
return Livewire::actingAs($user)->test(Application::class);
}
/*
|--------------------------------------------------------------------------
| Component Rendering & Mount
|--------------------------------------------------------------------------
*/
it('renders page successfully', function () {
mountApplicationComponent($this->user)
->assertViewIs('livewire.studio.setting.application')
->assertViewHas('pageTitle', 'Aplikasi');
});
it('mounts with settings loaded', function () {
$component = mountApplicationComponent($this->user);
expect($component->generalForm->site_name)->toBe('Yadi Parfum');
expect($component->contactForm->phone_number)->toBe('083816931711');
expect($component->socialForm->whatsapp)->toBe('https://wa.me/6283816931711');
expect($component->seoForm->meta_description)->toBe('Nikmati pengalaman wangi berkualitas tinggi dengan parfum refill dan produk unggulan dari Yadi Parfum.');
});
/*
|--------------------------------------------------------------------------
| General Settings Update
|--------------------------------------------------------------------------
*/
it('updates general settings successfully', function () {
mountApplicationComponent($this->user)
->set('generalForm.site_name', 'Updated Site Name')
->set('generalForm.site_tagline', 'Updated Tagline')
->set('generalForm.site_description', 'Updated description for the site')
->set('generalForm.maintenance_mode', true)
->call('updateGeneral')
->assertHasNoErrors();
$settings = app(GeneralSettings::class);
expect($settings->site_name)->toBe('Updated Site Name');
expect($settings->site_tagline)->toBe('Updated Tagline');
expect($settings->site_description)->toBe('Updated description for the site');
expect($settings->maintenance_mode)->toBe(true);
});
it('validates general settings required fields', function () {
mountApplicationComponent($this->user)
->set('generalForm.site_name', '')
->set('generalForm.site_tagline', '')
->set('generalForm.site_description', '')
->call('updateGeneral')
->assertHasErrors([
'generalForm.site_name' => 'required',
'generalForm.site_tagline' => 'required',
'generalForm.site_description' => 'required',
]);
});
it('validates general settings field lengths', function () {
mountApplicationComponent($this->user)
->set('generalForm.site_name', str_repeat('a', 101))
->set('generalForm.site_tagline', str_repeat('a', 151))
->call('updateGeneral')
->assertHasErrors([
'generalForm.site_name' => 'max',
'generalForm.site_tagline' => 'max',
]);
});
/*
|--------------------------------------------------------------------------
| Contact Settings Update
|--------------------------------------------------------------------------
*/
it('updates contact settings successfully', function () {
mountApplicationComponent($this->user)
->set('contactForm.phone_number', '0812 3456 7890')
->set('contactForm.email', 'contact@example.com')
->set('contactForm.address', 'Updated Address 123')
->set('contactForm.map_embed', '<iframe>map</iframe>')
->call('updateContact')
->assertHasNoErrors();
$settings = app(ContactSettings::class);
expect($settings->phone_number)->toBe('0812 3456 7890');
expect($settings->email)->toBe('contact@example.com');
expect($settings->address)->toBe('Updated Address 123');
expect($settings->map_embed)->toBe('<iframe>map</iframe>');
});
it('validates contact settings required fields', function () {
mountApplicationComponent($this->user)
->set('contactForm.phone_number', '')
->set('contactForm.address', '')
->call('updateContact')
->assertHasErrors([
'contactForm.phone_number' => 'required',
'contactForm.address' => 'required',
]);
});
it('validates contact settings email format', function () {
mountApplicationComponent($this->user)
->set('contactForm.phone_number', '+628123456789')
->set('contactForm.email', 'invalid-email')
->set('contactForm.address', 'Valid Address')
->call('updateContact')
->assertHasErrors([
'contactForm.email' => 'email',
]);
});
/*
|--------------------------------------------------------------------------
| Social Settings Update
|--------------------------------------------------------------------------
*/
it('updates social settings successfully', function () {
mountApplicationComponent($this->user)
->set('socialForm.facebook', 'https://facebook.com/page')
->set('socialForm.instagram', 'https://instagram.com/page')
->set('socialForm.youtube', 'https://youtube.com/channel')
->set('socialForm.tiktok', 'https://tiktok.com/@user')
->set('socialForm.whatsapp', 'https://wa.me/123456789')
->set('socialForm.telegram', 'https://t.me/channel')
->call('updateSocial')
->assertHasNoErrors();
$settings = app(SocialSettings::class);
expect($settings->facebook)->toBe('https://facebook.com/page');
expect($settings->instagram)->toBe('https://instagram.com/page');
expect($settings->youtube)->toBe('https://youtube.com/channel');
expect($settings->tiktok)->toBe('https://tiktok.com/@user');
expect($settings->whatsapp)->toBe('https://wa.me/123456789');
expect($settings->telegram)->toBe('https://t.me/channel');
});
it('validates social settings url format', function () {
mountApplicationComponent($this->user)
->set('socialForm.facebook', 'not-a-url')
->call('updateSocial')
->assertHasErrors([
'socialForm.facebook' => 'url',
]);
});
/*
|--------------------------------------------------------------------------
| SEO Settings Update
|--------------------------------------------------------------------------
*/
it('updates seo settings successfully', function () {
mountApplicationComponent($this->user)
->set('seoForm.meta_description', 'Updated meta description for SEO')
->set('seoForm.meta_keywords', 'keyword1, keyword2, keyword3')
->set('seoForm.meta_author', 'Updated Author')
->set('seoForm.og_image', 'https://example.com/image.jpg')
->set('seoForm.og_title', 'Open Graph Title')
->set('seoForm.og_description', 'Open Graph Description')
->set('seoForm.og_type', 'article')
->set('seoForm.twitter_card', 'summary_large_image')
->set('seoForm.canonical_url', 'https://example.com/page')
->call('updateSeo')
->assertHasNoErrors();
$settings = app(SeoSettings::class);
expect($settings->meta_description)->toBe('Updated meta description for SEO');
expect($settings->meta_keywords)->toBe('keyword1, keyword2, keyword3');
expect($settings->meta_author)->toBe('Updated Author');
expect($settings->og_image)->toBe('https://example.com/image.jpg');
expect($settings->og_title)->toBe('Open Graph Title');
expect($settings->og_description)->toBe('Open Graph Description');
expect($settings->og_type)->toBe('article');
expect($settings->twitter_card)->toBe('summary_large_image');
expect($settings->canonical_url)->toBe('https://example.com/page');
});
it('validates seo settings required fields', function () {
mountApplicationComponent($this->user)
->set('seoForm.meta_description', '')
->set('seoForm.meta_keywords', '')
->set('seoForm.meta_author', '')
->call('updateSeo')
->assertHasErrors([
'seoForm.meta_description' => 'required',
'seoForm.meta_keywords' => 'required',
'seoForm.meta_author' => 'required',
]);
});
it('validates seo settings og_type enum', function () {
mountApplicationComponent($this->user)
->set('seoForm.meta_description', 'Valid description')
->set('seoForm.meta_keywords', 'keywords')
->set('seoForm.meta_author', 'Author')
->set('seoForm.og_type', 'invalid_type')
->call('updateSeo')
->assertHasErrors([
'seoForm.og_type' => 'in',
]);
});
it('validates seo settings twitter_card enum', function () {
mountApplicationComponent($this->user)
->set('seoForm.meta_description', 'Valid description')
->set('seoForm.meta_keywords', 'keywords')
->set('seoForm.meta_author', 'Author')
->set('seoForm.twitter_card', 'invalid_card')
->call('updateSeo')
->assertHasErrors([
'seoForm.twitter_card' => 'in',
]);
});
/*
|--------------------------------------------------------------------------
| Authorization Tests
|--------------------------------------------------------------------------
*/
it('requires update general settings permission', function () {
// Temporarily remove the permission from the user
$this->ownerRole->revokePermissionTo('update general settings');
mountApplicationComponent($this->user)
->call('updateGeneral')
->assertForbidden();
// Restore the permission
$this->user->givePermissionTo('update general settings');
});
it('requires update contact settings permission', function () {
// Temporarily remove the permission from the user
$this->ownerRole->revokePermissionTo('update contact settings');
mountApplicationComponent($this->user)
->call('updateContact')
->assertForbidden();
// Restore the permission
$this->user->givePermissionTo('update contact settings');
});
it('requires update social settings permission', function () {
// Temporarily remove the permission from the user
$this->ownerRole->revokePermissionTo('update social settings');
mountApplicationComponent($this->user)
->call('updateSocial')
->assertForbidden();
// Restore the permission
$this->user->givePermissionTo('update social settings');
});
it('requires update seo settings permission', function () {
// Temporarily remove the permission from the user
$this->ownerRole->revokePermissionTo('update seo settings');
mountApplicationComponent($this->user)
->call('updateSeo')
->assertForbidden();
// Restore the permission
$this->user->givePermissionTo('update seo settings');
});

View File

@ -1,201 +0,0 @@
<?php
use App\Livewire\Forms\Studio\Setting\ContactForm;
use Illuminate\Support\Facades\Validator;
use Livewire\Livewire;
function createContactForm()
{
// Create a minimal Livewire component for testing
$component = new class extends \Livewire\Component
{
public ContactForm $form;
public function mount()
{
$this->form = new ContactForm($this, 'form');
}
public function render()
{
return '<div></div>';
}
};
$component->mount();
return $component->form;
}
it('has correct validation rules', function () {
$form = createContactForm();
$rules = $form->rules();
expect($rules)->toHaveKey('phone_number');
expect($rules)->toHaveKey('email');
expect($rules)->toHaveKey('address');
expect($rules)->toHaveKey('map_embed');
expect($rules['phone_number'])->toContain('required');
expect($rules['phone_number'])->toContain('string');
expect($rules['phone_number'])->toContain('min:10');
expect($rules['phone_number'])->toContain('max:20');
expect($rules['email'])->toContain('nullable');
expect($rules['email'])->toContain('email');
expect($rules['email'])->toContain('max:100');
expect($rules['address'])->toContain('required');
expect($rules['address'])->toContain('string');
expect($rules['address'])->toContain('min:10');
expect($rules['address'])->toContain('max:500');
expect($rules['map_embed'])->toContain('nullable');
expect($rules['map_embed'])->toContain('string');
expect($rules['map_embed'])->toContain('max:2000');
});
it('validates required fields', function () {
$form = createContactForm();
$rules = $form->rules();
$validator = Validator::make([
'phone_number' => '',
'address' => '',
], $rules);
expect($validator->fails())->toBeTrue();
expect($validator->errors()->has('phone_number'))->toBeTrue();
expect($validator->errors()->has('address'))->toBeTrue();
});
it('validates phone number format', function () {
$form = createContactForm();
$rules = $form->rules();
$validator = Validator::make([
'phone_number' => 'invalid',
'address' => 'Valid Address',
], $rules);
expect($validator->fails())->toBeTrue();
expect($validator->errors()->has('phone_number'))->toBeTrue();
});
it('validates email format', function () {
$form = createContactForm();
$rules = $form->rules();
$validator = Validator::make([
'phone_number' => '0812 3456 789',
'email' => 'invalid-email',
'address' => 'Valid Address',
], $rules);
expect($validator->fails())->toBeTrue();
expect($validator->errors()->has('email'))->toBeTrue();
});
it('validates phone number length', function () {
$form = createContactForm();
$rules = $form->rules();
$validator = Validator::make([
'phone_number' => '123',
'address' => 'Valid Address',
], $rules);
expect($validator->fails())->toBeTrue();
expect($validator->errors()->has('phone_number'))->toBeTrue();
});
it('validates address length', function () {
$form = createContactForm();
$rules = $form->rules();
$validator = Validator::make([
'phone_number' => '+628123456789',
'address' => 'Short',
], $rules);
expect($validator->fails())->toBeTrue();
expect($validator->errors()->has('address'))->toBeTrue();
});
it('validates email max length', function () {
$form = createContactForm();
$rules = $form->rules();
$validator = Validator::make([
'phone_number' => '+628123456789',
'email' => str_repeat('a', 101).'@example.com',
'address' => 'Valid Address',
], $rules);
expect($validator->fails())->toBeTrue();
expect($validator->errors()->has('email'))->toBeTrue();
});
it('validates map embed max length', function () {
$form = createContactForm();
$rules = $form->rules();
$validator = Validator::make([
'phone_number' => '+628123456789',
'address' => 'Valid Address',
'map_embed' => str_repeat('a', 2001),
], $rules);
expect($validator->fails())->toBeTrue();
expect($validator->errors()->has('map_embed'))->toBeTrue();
});
it('accepts nullable email', function () {
$form = createContactForm();
$rules = $form->rules();
$validator = Validator::make([
'phone_number' => '0812 3456 789',
'email' => null,
'address' => 'Valid Address',
], $rules);
expect($validator->passes())->toBeTrue();
});
it('accepts nullable map embed', function () {
$form = createContactForm();
$rules = $form->rules();
$validator = Validator::make([
'phone_number' => '0812 3456 789',
'address' => 'Valid Address',
'map_embed' => null,
], $rules);
expect($validator->passes())->toBeTrue();
});
it('passes validation with valid data', function () {
$form = createContactForm();
$rules = $form->rules();
$validator = Validator::make([
'phone_number' => '0812 3456 789',
'email' => 'test@example.com',
'address' => 'Valid Address with proper length',
'map_embed' => '<iframe>valid embed</iframe>',
], $rules);
expect($validator->passes())->toBeTrue();
});
it('has correct validation attributes', function () {
$form = createContactForm();
$attributes = $form->validationAttributes();
expect($attributes)->toHaveKey('phone_number', 'nomor telepon');
expect($attributes)->toHaveKey('email', 'email');
expect($attributes)->toHaveKey('address', 'alamat');
expect($attributes)->toHaveKey('map_embed', 'embed peta');
});

View File

@ -1,143 +0,0 @@
<?php
use App\Livewire\Forms\Studio\Setting\GeneralForm;
use Illuminate\Support\Facades\Validator;
use Livewire\Livewire;
function createGeneralForm()
{
// Create a minimal Livewire component for testing
$component = new class extends \Livewire\Component
{
public GeneralForm $form;
public function mount()
{
$this->form = new GeneralForm($this, 'form');
}
public function render()
{
return '<div></div>';
}
};
$component->mount();
return $component->form;
}
it('has correct validation rules', function () {
$form = createGeneralForm();
$rules = $form->rules();
expect($rules)->toHaveKey('site_name');
expect($rules)->toHaveKey('site_tagline');
expect($rules)->toHaveKey('site_description');
expect($rules)->toHaveKey('maintenance_mode');
expect($rules['site_name'])->toContain('required');
expect($rules['site_name'])->toContain('string');
expect($rules['site_name'])->toContain('max:100');
expect($rules['site_name'])->toContain('min:1');
expect($rules['site_tagline'])->toContain('required');
expect($rules['site_tagline'])->toContain('string');
expect($rules['site_tagline'])->toContain('max:150');
expect($rules['site_tagline'])->toContain('min:1');
expect($rules['site_description'])->toContain('required');
expect($rules['site_description'])->toContain('string');
expect($rules['site_description'])->toContain('min:10');
expect($rules['site_description'])->toContain('max:500');
expect($rules['maintenance_mode'])->toContain('required');
expect($rules['maintenance_mode'])->toContain('boolean');
});
it('validates required fields', function () {
$form = createGeneralForm();
$rules = $form->rules();
$validator = Validator::make([
'site_name' => '',
'site_tagline' => '',
'site_description' => '',
'maintenance_mode' => false,
], $rules);
expect($validator->fails())->toBeTrue();
expect($validator->errors()->has('site_name'))->toBeTrue();
expect($validator->errors()->has('site_tagline'))->toBeTrue();
expect($validator->errors()->has('site_description'))->toBeTrue();
});
it('validates minimum lengths', function () {
$form = createGeneralForm();
$rules = $form->rules();
$validator = Validator::make([
'site_name' => 'A',
'site_tagline' => 'B',
'site_description' => 'Short',
'maintenance_mode' => false,
], $rules);
expect($validator->fails())->toBeTrue();
expect($validator->errors()->has('site_description'))->toBeTrue();
});
it('validates maximum lengths', function () {
$form = createGeneralForm();
$rules = $form->rules();
$validator = Validator::make([
'site_name' => str_repeat('A', 101),
'site_tagline' => str_repeat('B', 151),
'site_description' => 'Valid description',
'maintenance_mode' => false,
], $rules);
expect($validator->fails())->toBeTrue();
expect($validator->errors()->has('site_name'))->toBeTrue();
expect($validator->errors()->has('site_tagline'))->toBeTrue();
});
it('validates maintenance mode boolean', function () {
$form = createGeneralForm();
$rules = $form->rules();
$validator = Validator::make([
'site_name' => 'Valid Name',
'site_tagline' => 'Valid Tagline',
'site_description' => 'Valid description with proper length',
'maintenance_mode' => 'not_boolean',
], $rules);
expect($validator->fails())->toBeTrue();
expect($validator->errors()->has('maintenance_mode'))->toBeTrue();
});
it('passes validation with valid data', function () {
$form = createGeneralForm();
$rules = $form->rules();
$validator = Validator::make([
'site_name' => 'Valid Site Name',
'site_tagline' => 'Valid Tagline',
'site_description' => 'Valid description with proper length for the site',
'maintenance_mode' => true,
], $rules);
expect($validator->passes())->toBeTrue();
});
it('has correct validation attributes', function () {
$form = createGeneralForm();
$attributes = $form->validationAttributes();
expect($attributes)->toHaveKey('site_name', 'nama situs');
expect($attributes)->toHaveKey('site_tagline', 'tagline situs');
expect($attributes)->toHaveKey('site_description', 'deskripsi situs');
expect($attributes)->toHaveKey('maintenance_mode', 'mode maintenance');
});

View File

@ -1,350 +0,0 @@
<?php
use App\Livewire\Forms\Studio\Setting\SeoForm;
use Illuminate\Support\Facades\Validator;
use Livewire\Livewire;
function createSeoForm()
{
// Create a minimal Livewire component for testing
$component = new class extends \Livewire\Component
{
public SeoForm $form;
public function mount()
{
$this->form = new SeoForm($this, 'form');
}
public function render()
{
return '<div></div>';
}
};
$component->mount();
return $component->form;
}
it('has correct validation rules', function () {
$form = createSeoForm();
$rules = $form->rules();
expect($rules)->toHaveKey('meta_description');
expect($rules)->toHaveKey('meta_keywords');
expect($rules)->toHaveKey('meta_author');
expect($rules)->toHaveKey('og_image');
expect($rules)->toHaveKey('og_title');
expect($rules)->toHaveKey('og_description');
expect($rules)->toHaveKey('og_type');
expect($rules)->toHaveKey('twitter_card');
expect($rules)->toHaveKey('canonical_url');
expect($rules['meta_description'])->toContain('required');
expect($rules['meta_description'])->toContain('string');
expect($rules['meta_description'])->toContain('min:10');
expect($rules['meta_description'])->toContain('max:160');
expect($rules['meta_keywords'])->toContain('required');
expect($rules['meta_keywords'])->toContain('string');
expect($rules['meta_keywords'])->toContain('min:3');
expect($rules['meta_keywords'])->toContain('max:255');
expect($rules['meta_author'])->toContain('required');
expect($rules['meta_author'])->toContain('string');
expect($rules['meta_author'])->toContain('min:1');
expect($rules['meta_author'])->toContain('max:150');
expect($rules['og_image'])->toContain('nullable');
expect($rules['og_image'])->toContain('string');
expect($rules['og_image'])->toContain('max:500');
expect($rules['og_image'])->toContain('url');
expect($rules['og_title'])->toContain('nullable');
expect($rules['og_title'])->toContain('string');
expect($rules['og_title'])->toContain('max:255');
expect($rules['og_description'])->toContain('nullable');
expect($rules['og_description'])->toContain('string');
expect($rules['og_description'])->toContain('max:300');
expect($rules['og_type'])->toContain('nullable');
expect($rules['og_type'])->toContain('string');
expect($rules['og_type'])->toContain('max:50');
expect($rules['og_type'])->toContain('in:website,article,product');
expect($rules['twitter_card'])->toContain('nullable');
expect($rules['twitter_card'])->toContain('string');
expect($rules['twitter_card'])->toContain('max:50');
expect($rules['twitter_card'])->toContain('in:summary,summary_large_image');
expect($rules['canonical_url'])->toContain('nullable');
expect($rules['canonical_url'])->toContain('string');
expect($rules['canonical_url'])->toContain('max:500');
expect($rules['canonical_url'])->toContain('url');
});
it('validates required fields', function () {
$form = createSeoForm();
$rules = $form->rules();
$validator = Validator::make([
'meta_description' => '',
'meta_keywords' => '',
'meta_author' => '',
], $rules);
expect($validator->fails())->toBeTrue();
expect($validator->errors()->has('meta_description'))->toBeTrue();
expect($validator->errors()->has('meta_keywords'))->toBeTrue();
expect($validator->errors()->has('meta_author'))->toBeTrue();
});
it('validates meta description length', function () {
$form = createSeoForm();
$rules = $form->rules();
$validator = Validator::make([
'meta_description' => 'Short',
'meta_keywords' => 'keywords',
'meta_author' => 'Author',
], $rules);
expect($validator->fails())->toBeTrue();
expect($validator->errors()->has('meta_description'))->toBeTrue();
});
it('validates meta description max length', function () {
$form = createSeoForm();
$rules = $form->rules();
$validator = Validator::make([
'meta_description' => str_repeat('a', 161),
'meta_keywords' => 'keywords',
'meta_author' => 'Author',
], $rules);
expect($validator->fails())->toBeTrue();
expect($validator->errors()->has('meta_description'))->toBeTrue();
});
it('validates meta keywords minimum length', function () {
$form = createSeoForm();
$rules = $form->rules();
$validator = Validator::make([
'meta_description' => 'Valid meta description with proper length',
'meta_keywords' => 'ab',
'meta_author' => 'Author',
], $rules);
expect($validator->fails())->toBeTrue();
expect($validator->errors()->has('meta_keywords'))->toBeTrue();
});
it('validates meta author max length', function () {
$form = createSeoForm();
$rules = $form->rules();
$validator = Validator::make([
'meta_description' => 'Valid meta description with proper length',
'meta_keywords' => 'valid keywords',
'meta_author' => str_repeat('a', 151),
], $rules);
expect($validator->fails())->toBeTrue();
expect($validator->errors()->has('meta_author'))->toBeTrue();
});
it('validates og_image url format and max length', function () {
$form = createSeoForm();
$rules = $form->rules();
$validator = Validator::make([
'meta_description' => 'Valid meta description with proper length',
'meta_keywords' => 'valid keywords',
'meta_author' => 'Author',
'og_image' => 'not-a-url',
], $rules);
expect($validator->fails())->toBeTrue();
expect($validator->errors()->has('og_image'))->toBeTrue();
});
it('validates og_image max length', function () {
$form = createSeoForm();
$rules = $form->rules();
$validator = Validator::make([
'meta_description' => 'Valid meta description with proper length',
'meta_keywords' => 'valid keywords',
'meta_author' => 'Author',
'og_image' => 'https://example.com/'.str_repeat('a', 501),
], $rules);
expect($validator->fails())->toBeTrue();
expect($validator->errors()->has('og_image'))->toBeTrue();
});
it('validates og_title max length', function () {
$form = createSeoForm();
$rules = $form->rules();
$validator = Validator::make([
'meta_description' => 'Valid meta description with proper length',
'meta_keywords' => 'valid keywords',
'meta_author' => 'Author',
'og_title' => str_repeat('a', 256),
], $rules);
expect($validator->fails())->toBeTrue();
expect($validator->errors()->has('og_title'))->toBeTrue();
});
it('validates og_description max length', function () {
$form = createSeoForm();
$rules = $form->rules();
$validator = Validator::make([
'meta_description' => 'Valid meta description with proper length',
'meta_keywords' => 'valid keywords',
'meta_author' => 'Author',
'og_description' => str_repeat('a', 301),
], $rules);
expect($validator->fails())->toBeTrue();
expect($validator->errors()->has('og_description'))->toBeTrue();
});
it('validates og_type enum values', function () {
$form = createSeoForm();
$rules = $form->rules();
$validator = Validator::make([
'meta_description' => 'Valid meta description with proper length',
'meta_keywords' => 'valid keywords',
'meta_author' => 'Author',
'og_type' => 'invalid_type',
], $rules);
expect($validator->fails())->toBeTrue();
expect($validator->errors()->has('og_type'))->toBeTrue();
});
it('accepts valid og_type values', function () {
$form = createSeoForm();
$rules = $form->rules();
$validTypes = ['website', 'article', 'product'];
foreach ($validTypes as $type) {
$validator = Validator::make([
'meta_description' => 'Valid meta description with proper length',
'meta_keywords' => 'valid keywords',
'meta_author' => 'Author',
'og_type' => $type,
], $rules);
expect($validator->passes())->toBeTrue();
}
});
it('validates twitter_card enum values', function () {
$form = createSeoForm();
$rules = $form->rules();
$validator = Validator::make([
'meta_description' => 'Valid meta description with proper length',
'meta_keywords' => 'valid keywords',
'meta_author' => 'Author',
'twitter_card' => 'invalid_card',
], $rules);
expect($validator->fails())->toBeTrue();
expect($validator->errors()->has('twitter_card'))->toBeTrue();
});
it('accepts valid twitter_card values', function () {
$form = createSeoForm();
$rules = $form->rules();
$validCards = ['summary', 'summary_large_image'];
foreach ($validCards as $card) {
$validator = Validator::make([
'meta_description' => 'Valid meta description with proper length',
'meta_keywords' => 'valid keywords',
'meta_author' => 'Author',
'twitter_card' => $card,
], $rules);
expect($validator->passes())->toBeTrue();
}
});
it('validates canonical_url url format and max length', function () {
$form = createSeoForm();
$rules = $form->rules();
$validator = Validator::make([
'meta_description' => 'Valid meta description with proper length',
'meta_keywords' => 'valid keywords',
'meta_author' => 'Author',
'canonical_url' => 'not-a-url',
], $rules);
expect($validator->fails())->toBeTrue();
expect($validator->errors()->has('canonical_url'))->toBeTrue();
});
it('accepts nullable fields', function () {
$form = createSeoForm();
$rules = $form->rules();
$validator = Validator::make([
'meta_description' => 'Valid meta description with proper length',
'meta_keywords' => 'valid keywords',
'meta_author' => 'Author',
'og_image' => null,
'og_title' => null,
'og_description' => null,
'og_type' => null,
'twitter_card' => null,
'canonical_url' => null,
], $rules);
expect($validator->passes())->toBeTrue();
});
it('passes validation with valid data', function () {
$form = createSeoForm();
$rules = $form->rules();
$validator = Validator::make([
'meta_description' => 'Valid meta description for SEO purposes',
'meta_keywords' => 'keyword1, keyword2, keyword3',
'meta_author' => 'Valid Author',
'og_image' => 'https://example.com/image.jpg',
'og_title' => 'Valid OG Title',
'og_description' => 'Valid OG Description',
'og_type' => 'article',
'twitter_card' => 'summary_large_image',
'canonical_url' => 'https://example.com/page',
], $rules);
expect($validator->passes())->toBeTrue();
});
it('has correct validation attributes', function () {
$form = createSeoForm();
$attributes = $form->validationAttributes();
expect($attributes)->toHaveKey('meta_description', 'meta deskripsi');
expect($attributes)->toHaveKey('meta_keywords', 'meta kata kunci');
expect($attributes)->toHaveKey('meta_author', 'meta penulis');
expect($attributes)->toHaveKey('og_image', 'gambar Open Graph');
expect($attributes)->toHaveKey('og_title', 'judul Open Graph');
expect($attributes)->toHaveKey('og_description', 'deskripsi Open Graph');
expect($attributes)->toHaveKey('og_type', 'tipe Open Graph');
expect($attributes)->toHaveKey('twitter_card', 'kartu Twitter');
expect($attributes)->toHaveKey('canonical_url', 'URL kanonis');
});

View File

@ -1,117 +0,0 @@
<?php
use App\Livewire\Forms\Studio\Setting\SocialForm;
use Illuminate\Support\Facades\Validator;
use Livewire\Livewire;
function createSocialForm()
{
// Create a minimal Livewire component for testing
$component = new class extends \Livewire\Component
{
public SocialForm $form;
public function mount()
{
$this->form = new SocialForm($this, 'form');
}
public function render()
{
return '<div></div>';
}
};
$component->mount();
return $component->form;
}
it('has correct validation rules', function () {
$form = createSocialForm();
$rules = $form->rules();
expect($rules)->toHaveKey('facebook');
expect($rules)->toHaveKey('instagram');
expect($rules)->toHaveKey('youtube');
expect($rules)->toHaveKey('tiktok');
expect($rules)->toHaveKey('whatsapp');
expect($rules)->toHaveKey('telegram');
foreach (['facebook', 'instagram', 'youtube', 'tiktok', 'whatsapp', 'telegram'] as $field) {
expect($rules[$field])->toContain('nullable');
expect($rules[$field])->toContain('url');
expect($rules[$field])->toContain('max:255');
}
});
it('validates nullable fields accept null values', function () {
$form = createSocialForm();
$rules = $form->rules();
$validator = Validator::make([
'facebook' => null,
'instagram' => null,
'youtube' => null,
'tiktok' => null,
'whatsapp' => null,
'telegram' => null,
], $rules);
expect($validator->passes())->toBeTrue();
});
it('validates url format for social media fields', function () {
$form = createSocialForm();
$rules = $form->rules();
$validator = Validator::make([
'facebook' => 'not-a-url',
'instagram' => 'https://instagram.com/valid',
'youtube' => 'invalid-url',
'tiktok' => 'https://tiktok.com/@valid',
'whatsapp' => 'not-a-url',
'telegram' => 'https://t.me/valid',
], $rules);
expect($validator->fails())->toBeTrue();
expect($validator->errors()->has('facebook'))->toBeTrue();
expect($validator->errors()->has('youtube'))->toBeTrue();
expect($validator->errors()->has('whatsapp'))->toBeTrue();
expect($validator->errors()->has('instagram'))->toBeFalse();
expect($validator->errors()->has('tiktok'))->toBeFalse();
expect($validator->errors()->has('telegram'))->toBeFalse();
});
it('validates url max length', function () {
$form = createSocialForm();
$rules = $form->rules();
$validator = Validator::make([
'facebook' => 'https://facebook.com/'.str_repeat('a', 256),
'instagram' => 'https://instagram.com/valid',
'youtube' => 'https://youtube.com/valid',
'tiktok' => 'https://tiktok.com/@valid',
'whatsapp' => 'https://wa.me/valid',
'telegram' => 'https://t.me/valid',
], $rules);
expect($validator->fails())->toBeTrue();
expect($validator->errors()->has('facebook'))->toBeTrue();
});
it('accepts valid urls', function () {
$form = createSocialForm();
$rules = $form->rules();
$validator = Validator::make([
'facebook' => 'https://facebook.com/page',
'instagram' => 'https://instagram.com/page',
'youtube' => 'https://youtube.com/channel',
'tiktok' => 'https://tiktok.com/@user',
'whatsapp' => 'https://wa.me/123456789',
'telegram' => 'https://t.me/channel',
], $rules);
expect($validator->passes())->toBeTrue();
});