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