diff --git a/tests/Feature/Livewire/Auth/ForgotPasswordTest.php b/tests/Feature/Livewire/Auth/ForgotPasswordTest.php deleted file mode 100644 index d7546c1..0000000 --- a/tests/Feature/Livewire/Auth/ForgotPasswordTest.php +++ /dev/null @@ -1,78 +0,0 @@ -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(); -}); diff --git a/tests/Feature/Livewire/Auth/LoginTest.php b/tests/Feature/Livewire/Auth/LoginTest.php deleted file mode 100644 index 3b26676..0000000 --- a/tests/Feature/Livewire/Auth/LoginTest.php +++ /dev/null @@ -1,250 +0,0 @@ -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); -}); diff --git a/tests/Feature/Livewire/Auth/RegisterTest.php b/tests/Feature/Livewire/Auth/RegisterTest.php deleted file mode 100644 index c301b67..0000000 --- a/tests/Feature/Livewire/Auth/RegisterTest.php +++ /dev/null @@ -1,417 +0,0 @@ -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']); -}); diff --git a/tests/Feature/Livewire/Auth/ResetPasswordTest.php b/tests/Feature/Livewire/Auth/ResetPasswordTest.php deleted file mode 100644 index 41ea889..0000000 --- a/tests/Feature/Livewire/Auth/ResetPasswordTest.php +++ /dev/null @@ -1,102 +0,0 @@ - $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(); -}); diff --git a/tests/Feature/Livewire/Studio/Catalog/Bottle/AuditTest.php b/tests/Feature/Livewire/Studio/Catalog/Bottle/AuditTest.php deleted file mode 100644 index 77b9916..0000000 --- a/tests/Feature/Livewire/Studio/Catalog/Bottle/AuditTest.php +++ /dev/null @@ -1,466 +0,0 @@ -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(); -}); diff --git a/tests/Feature/Livewire/Studio/Catalog/Bottle/CreateTest.php b/tests/Feature/Livewire/Studio/Catalog/Bottle/CreateTest.php deleted file mode 100644 index ad8f0f8..0000000 --- a/tests/Feature/Livewire/Studio/Catalog/Bottle/CreateTest.php +++ /dev/null @@ -1,344 +0,0 @@ -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); -}); diff --git a/tests/Feature/Livewire/Studio/Catalog/Bottle/EditTest.php b/tests/Feature/Livewire/Studio/Catalog/Bottle/EditTest.php deleted file mode 100644 index 9592f0f..0000000 --- a/tests/Feature/Livewire/Studio/Catalog/Bottle/EditTest.php +++ /dev/null @@ -1,344 +0,0 @@ -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'); -}); diff --git a/tests/Feature/Livewire/Studio/Catalog/Bottle/IndexTest.php b/tests/Feature/Livewire/Studio/Catalog/Bottle/IndexTest.php deleted file mode 100644 index bbb800e..0000000 --- a/tests/Feature/Livewire/Studio/Catalog/Bottle/IndexTest.php +++ /dev/null @@ -1,115 +0,0 @@ -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(); -}); diff --git a/tests/Feature/Livewire/Studio/Catalog/BrandTest.php b/tests/Feature/Livewire/Studio/Catalog/BrandTest.php deleted file mode 100644 index 4d8d79e..0000000 --- a/tests/Feature/Livewire/Studio/Catalog/BrandTest.php +++ /dev/null @@ -1,573 +0,0 @@ -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); -}); diff --git a/tests/Feature/Livewire/Studio/Catalog/CategoryTest.php b/tests/Feature/Livewire/Studio/Catalog/CategoryTest.php deleted file mode 100644 index dcee950..0000000 --- a/tests/Feature/Livewire/Studio/Catalog/CategoryTest.php +++ /dev/null @@ -1,925 +0,0 @@ -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'); -}); diff --git a/tests/Feature/Livewire/Studio/Catalog/Perfume/AuditTest.php b/tests/Feature/Livewire/Studio/Catalog/Perfume/AuditTest.php deleted file mode 100644 index 72c84ab..0000000 --- a/tests/Feature/Livewire/Studio/Catalog/Perfume/AuditTest.php +++ /dev/null @@ -1,475 +0,0 @@ -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(); -}); diff --git a/tests/Feature/Livewire/Studio/Catalog/Perfume/CreateTest.php b/tests/Feature/Livewire/Studio/Catalog/Perfume/CreateTest.php deleted file mode 100644 index 9b647e0..0000000 --- a/tests/Feature/Livewire/Studio/Catalog/Perfume/CreateTest.php +++ /dev/null @@ -1,455 +0,0 @@ -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); -}); diff --git a/tests/Feature/Livewire/Studio/Catalog/Perfume/EditTest.php b/tests/Feature/Livewire/Studio/Catalog/Perfume/EditTest.php deleted file mode 100644 index 57b2cbd..0000000 --- a/tests/Feature/Livewire/Studio/Catalog/Perfume/EditTest.php +++ /dev/null @@ -1,480 +0,0 @@ -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'); -}); diff --git a/tests/Feature/Livewire/Studio/Catalog/Perfume/IndexTest.php b/tests/Feature/Livewire/Studio/Catalog/Perfume/IndexTest.php deleted file mode 100644 index 8fc97d4..0000000 --- a/tests/Feature/Livewire/Studio/Catalog/Perfume/IndexTest.php +++ /dev/null @@ -1,118 +0,0 @@ -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(); -}); diff --git a/tests/Feature/Livewire/Studio/Catalog/Product/AuditTest.php b/tests/Feature/Livewire/Studio/Catalog/Product/AuditTest.php deleted file mode 100644 index f5ec1b0..0000000 --- a/tests/Feature/Livewire/Studio/Catalog/Product/AuditTest.php +++ /dev/null @@ -1,473 +0,0 @@ -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(); -}); diff --git a/tests/Feature/Livewire/Studio/Catalog/Product/CreateTest.php b/tests/Feature/Livewire/Studio/Catalog/Product/CreateTest.php deleted file mode 100644 index 7831aeb..0000000 --- a/tests/Feature/Livewire/Studio/Catalog/Product/CreateTest.php +++ /dev/null @@ -1,351 +0,0 @@ -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); -}); diff --git a/tests/Feature/Livewire/Studio/Catalog/Product/EditTest.php b/tests/Feature/Livewire/Studio/Catalog/Product/EditTest.php deleted file mode 100644 index 09fbb4a..0000000 --- a/tests/Feature/Livewire/Studio/Catalog/Product/EditTest.php +++ /dev/null @@ -1,359 +0,0 @@ -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'); -}); diff --git a/tests/Feature/Livewire/Studio/Catalog/Product/IndexTest.php b/tests/Feature/Livewire/Studio/Catalog/Product/IndexTest.php deleted file mode 100644 index a82a998..0000000 --- a/tests/Feature/Livewire/Studio/Catalog/Product/IndexTest.php +++ /dev/null @@ -1,120 +0,0 @@ -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(); -}); diff --git a/tests/Feature/Livewire/Studio/Feature/ChooseUsTest.php b/tests/Feature/Livewire/Studio/Feature/ChooseUsTest.php deleted file mode 100644 index 0c7244f..0000000 --- a/tests/Feature/Livewire/Studio/Feature/ChooseUsTest.php +++ /dev/null @@ -1,431 +0,0 @@ -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(); -}); diff --git a/tests/Feature/Livewire/Studio/Feature/PerfumeFeatureTest.php b/tests/Feature/Livewire/Studio/Feature/PerfumeFeatureTest.php deleted file mode 100644 index 4711856..0000000 --- a/tests/Feature/Livewire/Studio/Feature/PerfumeFeatureTest.php +++ /dev/null @@ -1,426 +0,0 @@ -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(); -}); diff --git a/tests/Feature/Livewire/Studio/Finance/ExpenseTest.php b/tests/Feature/Livewire/Studio/Finance/ExpenseTest.php deleted file mode 100644 index 4bdd0d5..0000000 --- a/tests/Feature/Livewire/Studio/Finance/ExpenseTest.php +++ /dev/null @@ -1,246 +0,0 @@ - '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(); -}); diff --git a/tests/Feature/Livewire/Studio/Information/BroadcastTest.php b/tests/Feature/Livewire/Studio/Information/BroadcastTest.php deleted file mode 100644 index cc27aa0..0000000 --- a/tests/Feature/Livewire/Studio/Information/BroadcastTest.php +++ /dev/null @@ -1,194 +0,0 @@ - '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); -}); diff --git a/tests/Feature/Livewire/Studio/Information/FaqTest.php b/tests/Feature/Livewire/Studio/Information/FaqTest.php deleted file mode 100644 index 4b0ab1d..0000000 --- a/tests/Feature/Livewire/Studio/Information/FaqTest.php +++ /dev/null @@ -1,194 +0,0 @@ - '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'); -}); diff --git a/tests/Feature/Livewire/Studio/Loyalty/CustomerTest.php b/tests/Feature/Livewire/Studio/Loyalty/CustomerTest.php deleted file mode 100644 index 313d2b1..0000000 --- a/tests/Feature/Livewire/Studio/Loyalty/CustomerTest.php +++ /dev/null @@ -1,643 +0,0 @@ -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(); -}); diff --git a/tests/Feature/Livewire/Studio/Loyalty/TierTest.php b/tests/Feature/Livewire/Studio/Loyalty/TierTest.php deleted file mode 100644 index 845df42..0000000 --- a/tests/Feature/Livewire/Studio/Loyalty/TierTest.php +++ /dev/null @@ -1,649 +0,0 @@ -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 -}); diff --git a/tests/Feature/Livewire/Studio/Loyalty/Voucher/CreateTest.php b/tests/Feature/Livewire/Studio/Loyalty/Voucher/CreateTest.php deleted file mode 100644 index 7043f2c..0000000 --- a/tests/Feature/Livewire/Studio/Loyalty/Voucher/CreateTest.php +++ /dev/null @@ -1,596 +0,0 @@ -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); -}); diff --git a/tests/Feature/Livewire/Studio/Loyalty/Voucher/EditTest.php b/tests/Feature/Livewire/Studio/Loyalty/Voucher/EditTest.php deleted file mode 100644 index 62a788f..0000000 --- a/tests/Feature/Livewire/Studio/Loyalty/Voucher/EditTest.php +++ /dev/null @@ -1,495 +0,0 @@ -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); -}); diff --git a/tests/Feature/Livewire/Studio/Loyalty/Voucher/IndexTest.php b/tests/Feature/Livewire/Studio/Loyalty/Voucher/IndexTest.php deleted file mode 100644 index 246712e..0000000 --- a/tests/Feature/Livewire/Studio/Loyalty/Voucher/IndexTest.php +++ /dev/null @@ -1,312 +0,0 @@ -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); -}); diff --git a/tests/Feature/Livewire/Studio/Manage/Article/CreateTest.php b/tests/Feature/Livewire/Studio/Manage/Article/CreateTest.php deleted file mode 100644 index 32f3b42..0000000 --- a/tests/Feature/Livewire/Studio/Manage/Article/CreateTest.php +++ /dev/null @@ -1,136 +0,0 @@ -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'); -}); diff --git a/tests/Feature/Livewire/Studio/Manage/Article/EditTest.php b/tests/Feature/Livewire/Studio/Manage/Article/EditTest.php deleted file mode 100644 index 9ca62ac..0000000 --- a/tests/Feature/Livewire/Studio/Manage/Article/EditTest.php +++ /dev/null @@ -1,177 +0,0 @@ -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(); -}); diff --git a/tests/Feature/Livewire/Studio/Manage/Article/IndexTest.php b/tests/Feature/Livewire/Studio/Manage/Article/IndexTest.php deleted file mode 100644 index 7cba691..0000000 --- a/tests/Feature/Livewire/Studio/Manage/Article/IndexTest.php +++ /dev/null @@ -1,245 +0,0 @@ -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); -}); diff --git a/tests/Feature/Livewire/Studio/Manage/Article/TestHelpers.php b/tests/Feature/Livewire/Studio/Manage/Article/TestHelpers.php deleted file mode 100644 index 5b31359..0000000 --- a/tests/Feature/Livewire/Studio/Manage/Article/TestHelpers.php +++ /dev/null @@ -1,4 +0,0 @@ - '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(); -}); diff --git a/tests/Feature/Livewire/Studio/Master/Outlet/CreateTest.php b/tests/Feature/Livewire/Studio/Master/Outlet/CreateTest.php deleted file mode 100644 index b7b1593..0000000 --- a/tests/Feature/Livewire/Studio/Master/Outlet/CreateTest.php +++ /dev/null @@ -1,425 +0,0 @@ -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(); -}); diff --git a/tests/Feature/Livewire/Studio/Master/Outlet/EditTest.php b/tests/Feature/Livewire/Studio/Master/Outlet/EditTest.php deleted file mode 100644 index 9c7d4ff..0000000 --- a/tests/Feature/Livewire/Studio/Master/Outlet/EditTest.php +++ /dev/null @@ -1,346 +0,0 @@ -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); -}); diff --git a/tests/Feature/Livewire/Studio/Master/Outlet/IndexTest.php b/tests/Feature/Livewire/Studio/Master/Outlet/IndexTest.php deleted file mode 100644 index 49ba1e6..0000000 --- a/tests/Feature/Livewire/Studio/Master/Outlet/IndexTest.php +++ /dev/null @@ -1,239 +0,0 @@ -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(); - } -}); diff --git a/tests/Feature/Livewire/Studio/Master/Outlet/TestHelpers.php b/tests/Feature/Livewire/Studio/Master/Outlet/TestHelpers.php deleted file mode 100644 index 6e8c2b2..0000000 --- a/tests/Feature/Livewire/Studio/Master/Outlet/TestHelpers.php +++ /dev/null @@ -1,37 +0,0 @@ -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(); -} diff --git a/tests/Feature/Livewire/Studio/Master/User/CreateTest.php b/tests/Feature/Livewire/Studio/Master/User/CreateTest.php deleted file mode 100644 index 7d96c95..0000000 --- a/tests/Feature/Livewire/Studio/Master/User/CreateTest.php +++ /dev/null @@ -1,651 +0,0 @@ -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(); -}); diff --git a/tests/Feature/Livewire/Studio/Master/User/EditTest.php b/tests/Feature/Livewire/Studio/Master/User/EditTest.php deleted file mode 100644 index 29fbcd5..0000000 --- a/tests/Feature/Livewire/Studio/Master/User/EditTest.php +++ /dev/null @@ -1,677 +0,0 @@ -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(); -}); diff --git a/tests/Feature/Livewire/Studio/Master/User/IndexTest.php b/tests/Feature/Livewire/Studio/Master/User/IndexTest.php deleted file mode 100644 index 2ac5e4a..0000000 --- a/tests/Feature/Livewire/Studio/Master/User/IndexTest.php +++ /dev/null @@ -1,229 +0,0 @@ -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(); - } -}); diff --git a/tests/Feature/Livewire/Studio/Setting/AccountTest.php b/tests/Feature/Livewire/Studio/Setting/AccountTest.php deleted file mode 100644 index d986175..0000000 --- a/tests/Feature/Livewire/Studio/Setting/AccountTest.php +++ /dev/null @@ -1,169 +0,0 @@ -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(); -}); diff --git a/tests/Feature/Livewire/Studio/Setting/ApplicationTest.php b/tests/Feature/Livewire/Studio/Setting/ApplicationTest.php deleted file mode 100644 index f296a0f..0000000 --- a/tests/Feature/Livewire/Studio/Setting/ApplicationTest.php +++ /dev/null @@ -1,301 +0,0 @@ -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', '') - ->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(''); -}); - -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'); -}); diff --git a/tests/Feature/Livewire/Studio/Setting/ContactFormTest.php b/tests/Feature/Livewire/Studio/Setting/ContactFormTest.php deleted file mode 100644 index 229457c..0000000 --- a/tests/Feature/Livewire/Studio/Setting/ContactFormTest.php +++ /dev/null @@ -1,201 +0,0 @@ -form = new ContactForm($this, 'form'); - } - - public function render() - { - return '
'; - } - }; - - $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' => '', - ], $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'); -}); diff --git a/tests/Feature/Livewire/Studio/Setting/GeneralFormTest.php b/tests/Feature/Livewire/Studio/Setting/GeneralFormTest.php deleted file mode 100644 index 4be197b..0000000 --- a/tests/Feature/Livewire/Studio/Setting/GeneralFormTest.php +++ /dev/null @@ -1,143 +0,0 @@ -form = new GeneralForm($this, 'form'); - } - - public function render() - { - return '
'; - } - }; - - $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'); -}); diff --git a/tests/Feature/Livewire/Studio/Setting/SeoFormTest.php b/tests/Feature/Livewire/Studio/Setting/SeoFormTest.php deleted file mode 100644 index 81b411c..0000000 --- a/tests/Feature/Livewire/Studio/Setting/SeoFormTest.php +++ /dev/null @@ -1,350 +0,0 @@ -form = new SeoForm($this, 'form'); - } - - public function render() - { - return '
'; - } - }; - - $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'); -}); diff --git a/tests/Feature/Livewire/Studio/Setting/SocialFormTest.php b/tests/Feature/Livewire/Studio/Setting/SocialFormTest.php deleted file mode 100644 index 7381ff6..0000000 --- a/tests/Feature/Livewire/Studio/Setting/SocialFormTest.php +++ /dev/null @@ -1,117 +0,0 @@ -form = new SocialForm($this, 'form'); - } - - public function render() - { - return '
'; - } - }; - - $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(); -});