chore: Delete numerous Livewire feature tests.
This commit is contained in:
parent
f657a5273f
commit
374f77897a
@ -1,78 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Auth\ForgotPassword;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Helpers
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
function attemptSendResetLink(string $email)
|
||||
{
|
||||
return Livewire::test(ForgotPassword::class)
|
||||
->set('form.email', $email)
|
||||
->call('sendLink');
|
||||
}
|
||||
|
||||
function mockPasswordResetStatus(string $status, string $email = 'user@example.com'): void
|
||||
{
|
||||
Password::shouldReceive('sendResetLink')
|
||||
->once()
|
||||
->with(['form.email' => $email])
|
||||
->andReturn($status);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Component Rendering
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders forgot password component successfully', function () {
|
||||
Livewire::test(ForgotPassword::class)
|
||||
->assertViewIs('livewire.auth.forgot-password')
|
||||
->assertSet('form.email', '');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Validation Rules
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates email field is required', function () {
|
||||
Livewire::test(ForgotPassword::class)
|
||||
->set('form.email', '')
|
||||
->call('sendLink')
|
||||
->assertHasErrors(['form.email' => 'required']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Send Reset Link
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('sends reset link successfully', function () {
|
||||
$email = 'user@example.com';
|
||||
|
||||
mockPasswordResetStatus(Password::ResetLinkSent, $email);
|
||||
|
||||
attemptSendResetLink($email)
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
|
||||
it('handles failure when sending reset link', function () {
|
||||
$email = 'user@example.com';
|
||||
|
||||
mockPasswordResetStatus('passwords.user', $email);
|
||||
|
||||
attemptSendResetLink($email)
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
@ -1,250 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Auth\Login;
|
||||
use App\Models\Employee;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
collect(['Developer', 'Owner', 'Leader', 'Admin', 'Partner', 'Customer'])
|
||||
->each(fn ($role) => Role::create(['name' => $role]));
|
||||
|
||||
$this->correctPassword = config('myconfig.password_default');
|
||||
|
||||
$this->wrongPassword = 'wrongpassword';
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Helpers
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
function makeUser(?string $role = null, bool $isActive = true): User
|
||||
{
|
||||
$factory = User::factory();
|
||||
|
||||
$factory = $isActive
|
||||
? $factory->active()
|
||||
: $factory->inactive();
|
||||
|
||||
$user = $factory
|
||||
->has(Employee::factory()->male()->permanent())
|
||||
->create();
|
||||
|
||||
if ($role) {
|
||||
$user->assignRole($role);
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
function attemptLogin(string $login, string $password)
|
||||
{
|
||||
return Livewire::test(Login::class)
|
||||
->set('form.login', $login)
|
||||
->set('form.password', $password)
|
||||
->call('auth');
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders login component successfully', function () {
|
||||
Livewire::test(Login::class)
|
||||
->assertViewIs('livewire.auth.login')
|
||||
->assertSet('form.login', '')
|
||||
->assertSet('form.password', '');
|
||||
});
|
||||
|
||||
it('mounts component without query parameters', function () {
|
||||
Livewire::test(Login::class)
|
||||
->assertSet('form.login', '')
|
||||
->assertSet('form.password', '');
|
||||
});
|
||||
|
||||
it('mounts component with message query parameter', function () {
|
||||
$message = 'Test message';
|
||||
|
||||
Livewire::withQueryParams(['message' => $message])
|
||||
->test(Login::class)
|
||||
->assertSet('form.login', '')
|
||||
->assertSet('form.password', '');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Validation Rules
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates required fields', function () {
|
||||
Livewire::test(Login::class)
|
||||
->set('form.login', '')
|
||||
->set('form.password', '')
|
||||
->call('auth')
|
||||
->assertHasErrors(['form.login', 'form.password']);
|
||||
});
|
||||
|
||||
it('validates login field is required', function () {
|
||||
Livewire::test(Login::class)
|
||||
->set('form.login', '')
|
||||
->set('form.password', 'password123')
|
||||
->call('auth')
|
||||
->assertHasErrors(['form.login']);
|
||||
});
|
||||
|
||||
it('validates password field is required', function () {
|
||||
Livewire::test(Login::class)
|
||||
->set('form.login', 'testuser')
|
||||
->set('form.password', '')
|
||||
->call('auth')
|
||||
->assertHasErrors(['form.password']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Failures
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('fails authentication with wrong credentials', function () {
|
||||
attemptLogin('nonexistent@example.com', $this->correctPassword);
|
||||
|
||||
expect(auth()->check())->toBeFalse();
|
||||
});
|
||||
|
||||
it('fails authentication with non-existent user', function () {
|
||||
attemptLogin('nonexistent@example.com', $this->wrongPassword);
|
||||
|
||||
expect(auth()->check())->toBeFalse();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authentication Works (email & username)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('successfully authenticates with email', function () {
|
||||
$user = makeUser('Developer');
|
||||
|
||||
attemptLogin($user->email, $this->correctPassword)->assertRedirect('/studio/dashboard/overview');
|
||||
|
||||
expect(auth()->check())->toBeTrue();
|
||||
expect(auth()->id())->toBe($user->id);
|
||||
});
|
||||
|
||||
it('successfully authenticates with username', function () {
|
||||
$user = makeUser('Developer');
|
||||
|
||||
attemptLogin($user->username, $this->correctPassword)->assertRedirect('/studio/dashboard/overview');
|
||||
|
||||
expect(auth()->check())->toBeTrue();
|
||||
expect(auth()->id())->toBe($user->id);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Prevent Inactive User Login
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('prevents login for inactive user', function () {
|
||||
$user = makeUser('Developer', false);
|
||||
|
||||
attemptLogin($user->email, $this->correctPassword);
|
||||
|
||||
expect(auth()->check())->toBeFalse();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Role Redirect Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('redirects developer to studio dashboard', function () {
|
||||
$user = makeUser('Developer');
|
||||
|
||||
attemptLogin($user->email, $this->correctPassword)->assertRedirect('/studio/dashboard/overview');
|
||||
|
||||
expect(auth()->check())->toBeTrue();
|
||||
expect(auth()->id())->toBe($user->id);
|
||||
});
|
||||
|
||||
it('redirects owner to studio dashboard', function () {
|
||||
$user = makeUser('Owner');
|
||||
|
||||
attemptLogin($user->email, $this->correctPassword)->assertRedirect('/studio/dashboard/overview');
|
||||
|
||||
expect(auth()->check())->toBeTrue();
|
||||
expect(auth()->id())->toBe($user->id);
|
||||
});
|
||||
|
||||
it('redirects leader to studio dashboard', function () {
|
||||
$user = makeUser('Leader');
|
||||
|
||||
attemptLogin($user->email, $this->correctPassword)->assertRedirect('/studio/dashboard/overview');
|
||||
|
||||
expect(auth()->check())->toBeTrue();
|
||||
expect(auth()->id())->toBe($user->id);
|
||||
});
|
||||
|
||||
it('redirects admin to studio dashboard', function () {
|
||||
$user = makeUser('Admin');
|
||||
|
||||
attemptLogin($user->email, $this->correctPassword)->assertRedirect('/studio/dashboard/overview');
|
||||
|
||||
expect(auth()->check())->toBeTrue();
|
||||
expect(auth()->id())->toBe($user->id);
|
||||
});
|
||||
|
||||
it('redirects partner to partner overview', function () {
|
||||
$user = makeUser('Partner');
|
||||
|
||||
attemptLogin($user->email, $this->correctPassword)->assertRedirect('/partner/overview');
|
||||
|
||||
expect(auth()->check())->toBeTrue();
|
||||
expect(auth()->id())->toBe($user->id);
|
||||
});
|
||||
|
||||
it('redirects customer to member overview', function () {
|
||||
$user = makeUser('Customer');
|
||||
|
||||
attemptLogin($user->email, $this->correctPassword)->assertRedirect('/member/overview');
|
||||
|
||||
expect(auth()->check())->toBeTrue();
|
||||
expect(auth()->id())->toBe($user->id);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Edge Case
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('redirects user without role to login', function () {
|
||||
$user = makeUser();
|
||||
|
||||
attemptLogin($user->email, $this->correctPassword)->assertRedirect('/login');
|
||||
|
||||
expect(auth()->check())->toBeTrue();
|
||||
expect(auth()->id())->toBe($user->id);
|
||||
});
|
||||
|
||||
it('regenerates session on successful login', function () {
|
||||
$user = makeUser('Developer');
|
||||
|
||||
attemptLogin($user->email, $this->correctPassword);
|
||||
|
||||
expect(auth()->check())->toBeTrue();
|
||||
expect(auth()->user()->id)->toBe($user->id);
|
||||
});
|
||||
@ -1,417 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\Gender;
|
||||
use App\Enums\UserStatus;
|
||||
use App\Livewire\Auth\Register;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Membership;
|
||||
use App\Models\PointRecord;
|
||||
use App\Models\ReferralCode;
|
||||
use App\Models\ReferralUsage;
|
||||
use App\Models\Tier;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\TierSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
collect(['Developer', 'Owner', 'Leader', 'Admin', 'Partner', 'Customer'])
|
||||
->each(fn ($role) => Role::create(['name' => $role]));
|
||||
|
||||
$this->seed(TierSeeder::class);
|
||||
|
||||
Notification::fake();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Helpers
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
function fillStep1(?string $email = null, ?string $username = null, ?string $password = null)
|
||||
{
|
||||
return Livewire::test(Register::class)
|
||||
->set('form.email', $email ?? 'info.pangestuyoga@gmail.com')
|
||||
->set('form.username', $username ?? 'pangestu')
|
||||
->set('form.password', $password ?? 'Aggygga13@');
|
||||
}
|
||||
|
||||
function fillStep2(?string $name = null, ?string $phoneNumber = null, $gender = null, ?string $referralCode = null)
|
||||
{
|
||||
$component = fillStep1()
|
||||
->set('form.name', $name ?? 'Test User')
|
||||
->set('form.phone_number', $phoneNumber ?? '0812 3456 789')
|
||||
->set('form.gender', $gender ?? Gender::MALE->value);
|
||||
|
||||
if ($referralCode !== null) {
|
||||
$component->set('form.referral_code', $referralCode);
|
||||
}
|
||||
|
||||
return $component;
|
||||
}
|
||||
|
||||
function attemptRegistration(?string $referralCode = null)
|
||||
{
|
||||
fillStep1()->call('nextStep');
|
||||
|
||||
return fillStep2(referralCode: $referralCode)
|
||||
->call('register');
|
||||
}
|
||||
|
||||
function makeReferralCode(): ReferralCode
|
||||
{
|
||||
$user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory()->male()->permanent())
|
||||
->has(ReferralCode::factory())
|
||||
->create();
|
||||
|
||||
$user->assignRole('Customer');
|
||||
|
||||
return $user->referralCode;
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders register component successfully', function () {
|
||||
Livewire::test(Register::class)
|
||||
->assertViewIs('livewire.auth.register')
|
||||
->assertSet('currentStep', 1)
|
||||
->assertSet('totalSteps', 2)
|
||||
->assertSet('form.email', '')
|
||||
->assertSet('form.username', '')
|
||||
->assertSet('form.password', '');
|
||||
});
|
||||
|
||||
it('mounts component with correct initial state', function () {
|
||||
Livewire::test(Register::class)
|
||||
->assertSet('currentStep', 1)
|
||||
->assertSet('totalSteps', 2)
|
||||
->assertCount('steps', 2)
|
||||
->assertSet('steps.0.name', 'Kredensial')
|
||||
->assertSet('steps.1.name', 'Informasi');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Step 1 Validation - Required Fields
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates step 1 required fields', function () {
|
||||
Livewire::test(Register::class)
|
||||
->set('form.email', '')
|
||||
->set('form.username', '')
|
||||
->set('form.password', '')
|
||||
->call('nextStep')
|
||||
->assertHasErrors(['form.email', 'form.username', 'form.password']);
|
||||
});
|
||||
|
||||
it('validates email field is required in step 1', function () {
|
||||
fillStep1('')
|
||||
->call('nextStep')
|
||||
->assertHasErrors(['form.email']);
|
||||
});
|
||||
|
||||
it('validates username field is required in step 1', function () {
|
||||
fillStep1(username: '')
|
||||
->call('nextStep')
|
||||
->assertHasErrors(['form.username']);
|
||||
});
|
||||
|
||||
it('validates password field is required in step 1', function () {
|
||||
fillStep1(password: '')
|
||||
->call('nextStep')
|
||||
->assertHasErrors(['form.password']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Step 1 Validation - Email Rules
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates email format in step 1', function () {
|
||||
fillStep1('invalid-email')
|
||||
->call('nextStep')
|
||||
->assertHasErrors(['form.email']);
|
||||
});
|
||||
|
||||
it('validates email is unique in step 1', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
fillStep1($user->email)
|
||||
->call('nextStep')
|
||||
->assertHasErrors(['form.email']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Step 1 Validation - Username Rules
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates username is unique in step 1', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
fillStep1(username: $user->username)
|
||||
->call('nextStep')
|
||||
->assertHasErrors(['form.username']);
|
||||
});
|
||||
|
||||
it('validates username is alpha numeric in step 1', function () {
|
||||
fillStep1(username: 'test user!')
|
||||
->call('nextStep')
|
||||
->assertHasErrors(['form.username']);
|
||||
});
|
||||
|
||||
it('validates username minimum length in step 1', function () {
|
||||
fillStep1(username: 'test')
|
||||
->call('nextStep')
|
||||
->assertHasErrors(['form.username']);
|
||||
});
|
||||
|
||||
it('validates username maximum length in step 1', function () {
|
||||
fillStep1(username: str_repeat('a', 21))
|
||||
->call('nextStep')
|
||||
->assertHasErrors(['form.username']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Step 1 Validation - Password Rules
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates password minimum length in step 1', function () {
|
||||
fillStep1(password: 'Pass1!')
|
||||
->call('nextStep')
|
||||
->assertHasErrors(['form.password']);
|
||||
});
|
||||
|
||||
it('validates password contains uppercase and lowercase letters in step 1', function () {
|
||||
fillStep1(password: 'password123!')
|
||||
->call('nextStep')
|
||||
->assertHasErrors(['form.password']);
|
||||
});
|
||||
|
||||
it('validates password contains numbers in step 1', function () {
|
||||
fillStep1(password: 'Password!')
|
||||
->call('nextStep')
|
||||
->assertHasErrors(['form.password']);
|
||||
});
|
||||
|
||||
it('validates password contains symbols in step 1', function () {
|
||||
fillStep1(password: 'Password123')
|
||||
->call('nextStep')
|
||||
->assertHasErrors(['form.password']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Step 1 Flow Control
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('moves to step 2 when step 1 validation passes', function () {
|
||||
fillStep1()
|
||||
->call('nextStep')
|
||||
->assertHasNoErrors()
|
||||
->assertSet('currentStep', 2);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Step 2 Validation - Required Fields
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates step 2 required fields', function () {
|
||||
fillStep1()
|
||||
->set('form.name', '')
|
||||
->set('form.phone_number', '')
|
||||
->set('form.gender', '')
|
||||
->call('register')
|
||||
->assertHasErrors(['form.name', 'form.phone_number', 'form.gender']);
|
||||
});
|
||||
|
||||
it('validates name field is required in step 2', function () {
|
||||
fillStep2('')
|
||||
->call('register')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates phone number field is required in step 2', function () {
|
||||
fillStep2(phoneNumber: '')
|
||||
->call('register')
|
||||
->assertHasErrors(['form.phone_number']);
|
||||
});
|
||||
|
||||
it('validates gender field is required in step 2', function () {
|
||||
fillStep2(gender: '')
|
||||
->call('register')
|
||||
->assertHasErrors(['form.gender']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Step 2 Validation - Field Format
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates phone number format in step 2', function () {
|
||||
fillStep2(phoneNumber: '08123456789')
|
||||
->call('register')
|
||||
->assertHasErrors(['form.phone_number']);
|
||||
});
|
||||
|
||||
it('validates gender is valid enum value in step 2', function () {
|
||||
fillStep2()
|
||||
->set('form.gender', 3)
|
||||
->call('register')
|
||||
->assertHasErrors(['form.gender']);
|
||||
});
|
||||
|
||||
it('validates referral code exists when provided in step 2', function () {
|
||||
fillStep2(referralCode: 'INVALIDCODE')
|
||||
->call('register')
|
||||
->assertHasErrors(['form.referral_code']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Step 2 Flow Control
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('can navigate back to step 1 from step 2', function () {
|
||||
fillStep1()
|
||||
->call('nextStep')
|
||||
->assertSet('currentStep', 2)
|
||||
->call('prevStep')
|
||||
->assertSet('currentStep', 1);
|
||||
});
|
||||
|
||||
it('cannot navigate before step 1', function () {
|
||||
Livewire::test(Register::class)
|
||||
->assertSet('currentStep', 1)
|
||||
->call('prevStep')
|
||||
->assertSet('currentStep', 1);
|
||||
});
|
||||
|
||||
it('can update step directly', function () {
|
||||
Livewire::test(Register::class)
|
||||
->call('updateStep', 2)
|
||||
->assertSet('currentStep', 1);
|
||||
});
|
||||
|
||||
it('validates step 1 when updating step directly to step 2', function () {
|
||||
Livewire::test(Register::class)
|
||||
->set('form.email', '')
|
||||
->set('form.username', '')
|
||||
->set('form.password', '')
|
||||
->call('updateStep', 2)
|
||||
->assertHasErrors(['form.email', 'form.username', 'form.password'])
|
||||
->assertSet('currentStep', 1);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Registration Success Cases
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('successfully registers a new user without referral code', function () {
|
||||
attemptRegistration()
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect('/member/overview');
|
||||
|
||||
expect(User::where('email', 'info.pangestuyoga@gmail.com')->exists())->toBeTrue();
|
||||
expect(Customer::whereHas('user', fn ($q) => $q->where('email', 'info.pangestuyoga@gmail.com'))->exists())->toBeTrue();
|
||||
|
||||
$user = User::where('email', 'info.pangestuyoga@gmail.com')->first();
|
||||
|
||||
expect($user->status)->toBe(UserStatus::ACTIVE);
|
||||
expect($user->hasRole('Customer'))->toBeTrue();
|
||||
expect(Membership::where('user_id', $user->id)->exists())->toBeTrue();
|
||||
expect(ReferralCode::where('user_id', $user->id)->exists())->toBeTrue();
|
||||
|
||||
expect(auth()->check())->toBeTrue();
|
||||
});
|
||||
|
||||
it('successfully registers a new user with referral code', function () {
|
||||
$referralCode = makeReferralCode();
|
||||
|
||||
attemptRegistration($referralCode->code)
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect('/member/overview');
|
||||
|
||||
$user = User::where('email', 'info.pangestuyoga@gmail.com')->first();
|
||||
expect(ReferralUsage::where('user_id', $user->id)->exists())->toBeTrue();
|
||||
|
||||
$referralCode->refresh();
|
||||
expect($referralCode->uses_count)->toBe(1);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Registration Data Creation
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('creates membership with correct tier and initial data', function () {
|
||||
attemptRegistration()
|
||||
->assertHasNoErrors();
|
||||
|
||||
$user = User::where('email', 'info.pangestuyoga@gmail.com')->first();
|
||||
$membership = Membership::where('user_id', $user->id)->first();
|
||||
$tier = Tier::orderBy('min_spending')->first();
|
||||
|
||||
expect($membership->tier_id)->toBe($tier->id);
|
||||
expect($membership->total_spending)->toBe(0);
|
||||
expect($membership->reward_points)->toBe(10);
|
||||
});
|
||||
|
||||
it('creates point record for registration', function () {
|
||||
attemptRegistration()
|
||||
->assertHasNoErrors();
|
||||
|
||||
$user = User::where('email', 'info.pangestuyoga@gmail.com')->first();
|
||||
$pointRecord = PointRecord::where('user_id', $user->id)->first();
|
||||
|
||||
expect($pointRecord->description)->toBe('Pendaftaran');
|
||||
expect($pointRecord->change)->toBe(10);
|
||||
expect($pointRecord->is_addition)->toBeTrue();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Real-time Validation
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates individual field on update for step 1', function () {
|
||||
Livewire::test(Register::class)
|
||||
->set('form.email', 'invalid-email')
|
||||
->assertHasErrors(['form.email'])
|
||||
->set('form.email', 'valid@example.com')
|
||||
->assertHasNoErrors(['form.email']);
|
||||
});
|
||||
|
||||
it('validates individual field on update for step 2', function () {
|
||||
fillStep1()->call('nextStep')
|
||||
->set('form.phone_number', 'invalid')
|
||||
->assertHasErrors(['form.phone_number'])
|
||||
->set('form.phone_number', '0812 3456 789')
|
||||
->assertHasNoErrors(['form.phone_number']);
|
||||
});
|
||||
@ -1,102 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Auth\ResetPassword;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Password;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Helpers
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
function makeResetComponent(string $token = 'sample-token', string $email = 'user@example.com')
|
||||
{
|
||||
return Livewire::withQueryParams([
|
||||
'token' => $token,
|
||||
'email' => $email,
|
||||
])->test(ResetPassword::class);
|
||||
}
|
||||
|
||||
function attemptResetPassword(
|
||||
string $token = 'sample-token',
|
||||
string $email = 'user@example.com',
|
||||
string $password = 'password123',
|
||||
string $passwordConfirmation = 'password123'
|
||||
) {
|
||||
return makeResetComponent($token, $email)
|
||||
->set('form.password', $password)
|
||||
->set('form.password_confirmation', $passwordConfirmation)
|
||||
->call('resetPassword');
|
||||
}
|
||||
|
||||
function mockPasswordReset(string $status): void
|
||||
{
|
||||
Password::shouldReceive('reset')
|
||||
->once()
|
||||
->andReturn($status);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders reset password component successfully', function () {
|
||||
$token = 'sample-token';
|
||||
$email = 'user@example.com';
|
||||
|
||||
makeResetComponent($token, $email)
|
||||
->assertViewIs('livewire.auth.reset-password')
|
||||
->assertSet('token', $token)
|
||||
->assertSet('email', $email);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Validation Rules
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates password fields are required', function () {
|
||||
makeResetComponent()
|
||||
->set('form.password', '')
|
||||
->set('form.password_confirmation', '')
|
||||
->call('resetPassword')
|
||||
->assertHasErrors(['form.password', 'form.password_confirmation']);
|
||||
});
|
||||
|
||||
it('validates password confirmation must match', function () {
|
||||
makeResetComponent()
|
||||
->set('form.password', 'password123')
|
||||
->set('form.password_confirmation', 'different123')
|
||||
->call('resetPassword')
|
||||
->assertHasErrors(['form.password' => 'confirmed']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Reset Password
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('resets password successfully', function () {
|
||||
mockPasswordReset(Password::PasswordReset);
|
||||
|
||||
attemptResetPassword()
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect(route('login', [
|
||||
'message' => 'Kata sandi berhasil diperbarui, silakan masuk kembali.',
|
||||
]));
|
||||
});
|
||||
|
||||
it('handles failure when resetting password', function () {
|
||||
mockPasswordReset('passwords.token');
|
||||
|
||||
attemptResetPassword()
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
@ -1,466 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\AuditStockStatus;
|
||||
use App\Models\Bottle;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
|
||||
$this->ownerRole = Role::where('name', 'Owner')->first();
|
||||
$this->user->roles()->attach($this->ownerRole->id);
|
||||
|
||||
// Create test outlets
|
||||
$this->outlets = Outlet::factory()->count(3)->create();
|
||||
});
|
||||
|
||||
function mountBottleAuditComponent(User $user, Outlet $outlet, Bottle $bottle)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(\App\Livewire\Studio\Catalog\Bottle\Audit::class, ['outlet' => $outlet, 'bottle' => $bottle]);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Audit Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('bottle audit component initializes correctly', function () {
|
||||
$bottle = Bottle::factory()->create(['name' => 'Test Bottle']);
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
$component = mountBottleAuditComponent($this->user, $outlet, $bottle);
|
||||
|
||||
expect($component->label)->toBe('Test Bottle di '.$outlet->name);
|
||||
expect($component->activityLogs)->toBeInstanceOf(\Illuminate\Database\Eloquent\Collection::class);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Activity Logs Display Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('displays bottle activity logs correctly', function () {
|
||||
$bottle = Bottle::factory()->create();
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
// Create some activity logs with expected properties
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock updated',
|
||||
'subject_type' => get_class($bottle),
|
||||
'subject_id' => $bottle->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 5,
|
||||
'quantity_change' => 10,
|
||||
'new_stock' => 15,
|
||||
],
|
||||
'event' => AuditStockStatus::INCREASED->value,
|
||||
]);
|
||||
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock decreased',
|
||||
'subject_type' => get_class($bottle),
|
||||
'subject_id' => $bottle->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 15,
|
||||
'quantity_change' => -5,
|
||||
'new_stock' => 10,
|
||||
],
|
||||
'event' => AuditStockStatus::DECREASED->value,
|
||||
]);
|
||||
|
||||
$component = mountBottleAuditComponent($this->user, $outlet, $bottle);
|
||||
|
||||
expect($component->activityLogs)->toHaveCount(2);
|
||||
expect($component->activityLogs->first()->event['label'])->toBe(AuditStockStatus::INCREASED->label());
|
||||
expect($component->activityLogs->first()->event['color'])->toBe(AuditStockStatus::INCREASED->color());
|
||||
expect($component->activityLogs->last()->event['label'])->toBe(AuditStockStatus::DECREASED->label());
|
||||
expect($component->activityLogs->last()->event['color'])->toBe(AuditStockStatus::DECREASED->color());
|
||||
});
|
||||
|
||||
it('filters bottle activity logs by outlet', function () {
|
||||
$bottle = Bottle::factory()->create();
|
||||
$outlet1 = $this->outlets->first();
|
||||
$outlet2 = $this->outlets->last();
|
||||
|
||||
// Create activity for outlet1
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock updated outlet1',
|
||||
'subject_type' => get_class($bottle),
|
||||
'subject_id' => $bottle->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet1->id,
|
||||
'previous_stock' => 5,
|
||||
'quantity_change' => 10,
|
||||
'new_stock' => 15,
|
||||
],
|
||||
'event' => AuditStockStatus::UPDATED->value,
|
||||
]);
|
||||
|
||||
// Create activity for outlet2
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock increased outlet2',
|
||||
'subject_type' => get_class($bottle),
|
||||
'subject_id' => $bottle->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet2->id,
|
||||
'previous_stock' => 0,
|
||||
'quantity_change' => 20,
|
||||
'new_stock' => 20,
|
||||
],
|
||||
'event' => AuditStockStatus::INCREASED->value,
|
||||
]);
|
||||
|
||||
$component1 = mountBottleAuditComponent($this->user, $outlet1, $bottle);
|
||||
$component2 = mountBottleAuditComponent($this->user, $outlet2, $bottle);
|
||||
|
||||
expect($component1->activityLogs)->toHaveCount(1);
|
||||
expect($component1->activityLogs->first()->description)->toBe('Stock updated outlet1');
|
||||
|
||||
expect($component2->activityLogs)->toHaveCount(1);
|
||||
expect($component2->activityLogs->first()->description)->toBe('Stock increased outlet2');
|
||||
});
|
||||
|
||||
it('orders bottle activity logs by latest first', function () {
|
||||
$bottle = Bottle::factory()->create();
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
// Create activities with different timestamps
|
||||
$activity1 = Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'first activity',
|
||||
'subject_type' => get_class($bottle),
|
||||
'subject_id' => $bottle->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 5,
|
||||
'quantity_change' => 5,
|
||||
'new_stock' => 10,
|
||||
],
|
||||
'event' => AuditStockStatus::UPDATED->value,
|
||||
'created_at' => now()->subMinutes(5),
|
||||
]);
|
||||
|
||||
$activity2 = Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'second activity',
|
||||
'subject_type' => get_class($bottle),
|
||||
'subject_id' => $bottle->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 10,
|
||||
'quantity_change' => 10,
|
||||
'new_stock' => 20,
|
||||
],
|
||||
'event' => AuditStockStatus::INCREASED->value,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
$component = mountBottleAuditComponent($this->user, $outlet, $bottle);
|
||||
|
||||
expect($component->activityLogs)->toHaveCount(2);
|
||||
expect($component->activityLogs->first()->description)->toBe('second activity');
|
||||
expect($component->activityLogs->last()->description)->toBe('first activity');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Activity Logs Content Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('maps bottle activity events to correct labels and colors', function () {
|
||||
$bottle = Bottle::factory()->create();
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
// Test INCREASED event
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock increased',
|
||||
'subject_type' => get_class($bottle),
|
||||
'subject_id' => $bottle->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 10,
|
||||
'quantity_change' => 5,
|
||||
'new_stock' => 15,
|
||||
],
|
||||
'event' => AuditStockStatus::INCREASED->value,
|
||||
]);
|
||||
|
||||
// Test DECREASED event
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock decreased',
|
||||
'subject_type' => get_class($bottle),
|
||||
'subject_id' => $bottle->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 15,
|
||||
'quantity_change' => -3,
|
||||
'new_stock' => 12,
|
||||
],
|
||||
'event' => AuditStockStatus::DECREASED->value,
|
||||
]);
|
||||
|
||||
// Test UPDATED event
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock updated',
|
||||
'subject_type' => get_class($bottle),
|
||||
'subject_id' => $bottle->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 12,
|
||||
'quantity_change' => 0,
|
||||
'new_stock' => 12,
|
||||
],
|
||||
'event' => AuditStockStatus::UPDATED->value,
|
||||
]);
|
||||
|
||||
$component = mountBottleAuditComponent($this->user, $outlet, $bottle);
|
||||
|
||||
expect($component->activityLogs)->toHaveCount(3);
|
||||
|
||||
// Check INCREASED event mapping
|
||||
$increasedLog = collect($component->activityLogs)->firstWhere('event.label', AuditStockStatus::INCREASED->label());
|
||||
expect($increasedLog['event']['color'])->toBe(AuditStockStatus::INCREASED->color());
|
||||
|
||||
// Check DECREASED event mapping
|
||||
$decreasedLog = collect($component->activityLogs)->firstWhere('event.label', AuditStockStatus::DECREASED->label());
|
||||
expect($decreasedLog['event']['color'])->toBe(AuditStockStatus::DECREASED->color());
|
||||
|
||||
// Check UPDATED event mapping
|
||||
$updatedLog = collect($component->activityLogs)->firstWhere('event.label', AuditStockStatus::UPDATED->label());
|
||||
expect($updatedLog['event']['color'])->toBe(AuditStockStatus::UPDATED->color());
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Empty State Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('handles empty bottle activity logs in audit component', function () {
|
||||
$bottle = Bottle::factory()->create();
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
$component = mountBottleAuditComponent($this->user, $outlet, $bottle);
|
||||
|
||||
expect($component->activityLogs)->toBeInstanceOf(\Illuminate\Database\Eloquent\Collection::class);
|
||||
expect($component->activityLogs)->toHaveCount(0);
|
||||
});
|
||||
|
||||
it('handles bottle with no activities in specific outlet', function () {
|
||||
$bottle = Bottle::factory()->create();
|
||||
$outlet1 = $this->outlets->first();
|
||||
$outlet2 = $this->outlets->last();
|
||||
|
||||
// Create activity only for outlet2
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock updated',
|
||||
'subject_type' => get_class($bottle),
|
||||
'subject_id' => $bottle->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet2->id,
|
||||
'previous_stock' => 5,
|
||||
'quantity_change' => 5,
|
||||
'new_stock' => 10,
|
||||
],
|
||||
'event' => AuditStockStatus::UPDATED->value,
|
||||
]);
|
||||
|
||||
$component1 = mountBottleAuditComponent($this->user, $outlet1, $bottle);
|
||||
$component2 = mountBottleAuditComponent($this->user, $outlet2, $bottle);
|
||||
|
||||
expect($component1->activityLogs)->toHaveCount(0);
|
||||
expect($component2->activityLogs)->toHaveCount(1);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Integration Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('displays real bottle activity log data correctly', function () {
|
||||
$bottle = Bottle::factory()->create(['name' => 'Integration Bottle']);
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
$activity = Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock adjustment for integration test',
|
||||
'subject_type' => get_class($bottle),
|
||||
'subject_id' => $bottle->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 100,
|
||||
'quantity_change' => 25,
|
||||
'new_stock' => 125,
|
||||
],
|
||||
'event' => AuditStockStatus::INCREASED->value,
|
||||
]);
|
||||
|
||||
$component = mountBottleAuditComponent($this->user, $outlet, $bottle);
|
||||
|
||||
expect($component->activityLogs)->toHaveCount(1);
|
||||
|
||||
$log = $component->activityLogs->first();
|
||||
expect($log->description)->toBe('Stock adjustment for integration test');
|
||||
expect($log->properties['previous_stock'])->toBe(100);
|
||||
expect($log->properties['quantity_change'])->toBe(25);
|
||||
expect($log->properties['new_stock'])->toBe(125);
|
||||
expect($log->event['label'])->toBe(AuditStockStatus::INCREASED->label());
|
||||
expect($log->event['color'])->toBe(AuditStockStatus::INCREASED->color());
|
||||
});
|
||||
|
||||
it('handles multiple bottle activities across different outlets', function () {
|
||||
$bottle = Bottle::factory()->create();
|
||||
$outlet1 = $this->outlets->first();
|
||||
$outlet2 = $this->outlets->last();
|
||||
|
||||
// Create activities for both outlets
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Activity in outlet 1',
|
||||
'subject_type' => get_class($bottle),
|
||||
'subject_id' => $bottle->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet1->id,
|
||||
'previous_stock' => 10,
|
||||
'quantity_change' => 5,
|
||||
'new_stock' => 15,
|
||||
],
|
||||
'event' => AuditStockStatus::INCREASED->value,
|
||||
]);
|
||||
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Activity in outlet 2',
|
||||
'subject_type' => get_class($bottle),
|
||||
'subject_id' => $bottle->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet2->id,
|
||||
'previous_stock' => 20,
|
||||
'quantity_change' => -3,
|
||||
'new_stock' => 17,
|
||||
],
|
||||
'event' => AuditStockStatus::DECREASED->value,
|
||||
]);
|
||||
|
||||
$component1 = mountBottleAuditComponent($this->user, $outlet1, $bottle);
|
||||
$component2 = mountBottleAuditComponent($this->user, $outlet2, $bottle);
|
||||
|
||||
expect($component1->activityLogs)->toHaveCount(1);
|
||||
expect($component1->activityLogs->first()->description)->toBe('Activity in outlet 1');
|
||||
|
||||
expect($component2->activityLogs)->toHaveCount(1);
|
||||
expect($component2->activityLogs->first()->description)->toBe('Activity in outlet 2');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Edge Cases and Error Handling
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('handles bottle activity logs with missing properties gracefully', function () {
|
||||
$bottle = Bottle::factory()->create();
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
// Create activity with incomplete properties (shouldn't happen in real usage)
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Incomplete activity',
|
||||
'subject_type' => get_class($bottle),
|
||||
'subject_id' => $bottle->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 0, // Add required properties
|
||||
'quantity_change' => 0,
|
||||
'new_stock' => 0,
|
||||
],
|
||||
'event' => AuditStockStatus::UPDATED->value,
|
||||
]);
|
||||
|
||||
$component = mountBottleAuditComponent($this->user, $outlet, $bottle);
|
||||
|
||||
expect($component->activityLogs)->toHaveCount(1);
|
||||
// Component should render correctly with all required properties
|
||||
});
|
||||
|
||||
it('handles bottle activities with null causer', function () {
|
||||
$bottle = Bottle::factory()->create();
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Activity with no causer',
|
||||
'subject_type' => get_class($bottle),
|
||||
'subject_id' => $bottle->id,
|
||||
'causer_type' => null,
|
||||
'causer_id' => null,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 5,
|
||||
'quantity_change' => 5,
|
||||
'new_stock' => 10,
|
||||
],
|
||||
'event' => AuditStockStatus::INCREASED->value,
|
||||
]);
|
||||
|
||||
$component = mountBottleAuditComponent($this->user, $outlet, $bottle);
|
||||
|
||||
expect($component->activityLogs)->toHaveCount(1);
|
||||
expect($component->activityLogs->first()->causer)->toBeNull();
|
||||
});
|
||||
@ -1,344 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Bottle;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
|
||||
$this->ownerRole = Role::where('name', 'Owner')->first();
|
||||
$this->user->roles()->attach($this->ownerRole->id);
|
||||
|
||||
// Create test outlets
|
||||
$this->outlets = Outlet::factory()->count(3)->create();
|
||||
});
|
||||
|
||||
function mountBottleCreateComponent(User $user)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(\App\Livewire\Studio\Catalog\Bottle\Create::class);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Create Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders bottle create page successfully', function () {
|
||||
mountBottleCreateComponent($this->user)
|
||||
->assertViewIs('livewire.studio.catalog.bottle.form')
|
||||
->assertViewHas('pageTitle', 'Tambah Botol');
|
||||
});
|
||||
|
||||
it('initializes bottle form properties on mount', function () {
|
||||
$component = mountBottleCreateComponent($this->user);
|
||||
|
||||
expect($component->form->name)->toBe('');
|
||||
expect($component->form->size)->toBe('');
|
||||
expect($component->form->cost_price)->toBe('');
|
||||
expect($component->form->sale_price)->toBe('');
|
||||
expect($component->form->description)->toBeNull();
|
||||
expect($component->form->outlet_ids)->toBe([]);
|
||||
expect($component->form->image)->toBe([]);
|
||||
expect($component->outlets)->toHaveCount(3);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Create Bottle Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('creates bottle successfully when authorized', function () {
|
||||
$data = [
|
||||
'name' => 'New Bottle',
|
||||
'size' => '100',
|
||||
'cost_price' => '50000',
|
||||
'sale_price' => '75000',
|
||||
'description' => 'Bottle description',
|
||||
'outlet_ids' => [$this->outlets->first()->id, $this->outlets->last()->id],
|
||||
];
|
||||
|
||||
mountBottleCreateComponent($this->user)
|
||||
->set('form.name', $data['name'])
|
||||
->set('form.size', $data['size'])
|
||||
->set('form.cost_price', $data['cost_price'])
|
||||
->set('form.sale_price', $data['sale_price'])
|
||||
->set('form.description', $data['description'])
|
||||
->set('form.outlet_ids', $data['outlet_ids'])
|
||||
->call('save')
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect(route('studio.catalog.bottle.index'));
|
||||
|
||||
expect(Bottle::count())->toBe(1);
|
||||
$bottle = Bottle::first();
|
||||
expect($bottle->name)->toBe($data['name']);
|
||||
expect($bottle->size)->toBe(100);
|
||||
expect($bottle->cost_price)->toBe(50000);
|
||||
expect($bottle->sale_price)->toBe(75000);
|
||||
expect($bottle->description)->toBe($data['description']);
|
||||
expect($bottle->outlets)->toHaveCount(2);
|
||||
});
|
||||
|
||||
it('prevents bottle creation when unauthorized', function () {
|
||||
$this->ownerRole->revokePermissionTo('create bottle');
|
||||
Gate::define('create bottle', fn () => false);
|
||||
|
||||
mountBottleCreateComponent($this->user)
|
||||
->set('form.name', 'Test Bottle')
|
||||
->set('form.size', '50')
|
||||
->set('form.sale_price', '25000')
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('creates bottle with empty cost_price for non-owner roles', function () {
|
||||
// Create user with limited role
|
||||
$this->user->roles()->detach();
|
||||
$limitedRole = Role::create(['name' => 'Cashier']);
|
||||
$this->user->roles()->attach($limitedRole->id);
|
||||
|
||||
$createPermission = Permission::where('name', 'create bottle')->first();
|
||||
$limitedRole->givePermissionTo($createPermission);
|
||||
|
||||
mountBottleCreateComponent($this->user)
|
||||
->set('form.name', 'Test Bottle')
|
||||
->set('form.size', '30')
|
||||
->set('form.cost_price', '') // Empty cost price
|
||||
->set('form.sale_price', '20000')
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$bottle = Bottle::first();
|
||||
expect($bottle->cost_price)->toBe(0);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Form Validation Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates required name field on bottle create', function () {
|
||||
mountBottleCreateComponent($this->user)
|
||||
->set('form.name', '')
|
||||
->set('form.size', '100')
|
||||
->set('form.sale_price', '50000')
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates name field max length on bottle create', function () {
|
||||
mountBottleCreateComponent($this->user)
|
||||
->set('form.name', str_repeat('a', 51))
|
||||
->set('form.size', '100')
|
||||
->set('form.sale_price', '50000')
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates required size field on bottle create', function () {
|
||||
mountBottleCreateComponent($this->user)
|
||||
->set('form.name', 'Test Bottle')
|
||||
->set('form.size', '')
|
||||
->set('form.sale_price', '50000')
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.size']);
|
||||
});
|
||||
|
||||
it('validates size is unsigned integer on bottle create', function () {
|
||||
mountBottleCreateComponent($this->user)
|
||||
->set('form.name', 'Test Bottle')
|
||||
->set('form.size', '-50') // Negative value
|
||||
->set('form.sale_price', '50000')
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.size']);
|
||||
});
|
||||
|
||||
it('validates required sale_price field on bottle create', function () {
|
||||
mountBottleCreateComponent($this->user)
|
||||
->set('form.name', 'Test Bottle')
|
||||
->set('form.size', '100')
|
||||
->set('form.sale_price', '')
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.sale_price']);
|
||||
});
|
||||
|
||||
it('validates sale_price is unsigned integer on bottle create', function () {
|
||||
mountBottleCreateComponent($this->user)
|
||||
->set('form.name', 'Test Bottle')
|
||||
->set('form.size', '100')
|
||||
->set('form.sale_price', '-50000') // Negative value
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.sale_price']);
|
||||
});
|
||||
|
||||
it('validates required outlet_ids field on bottle create', function () {
|
||||
mountBottleCreateComponent($this->user)
|
||||
->set('form.name', 'Test Bottle')
|
||||
->set('form.size', '100')
|
||||
->set('form.sale_price', '50000')
|
||||
->set('form.outlet_ids', [])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.outlet_ids']);
|
||||
});
|
||||
|
||||
it('validates outlet_ids contains valid outlet ids on bottle create', function () {
|
||||
mountBottleCreateComponent($this->user)
|
||||
->set('form.name', 'Test Bottle')
|
||||
->set('form.size', '100')
|
||||
->set('form.sale_price', '50000')
|
||||
->set('form.outlet_ids', [999])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.outlet_ids.*']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Real-time Validation Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates bottle name field in real-time', function () {
|
||||
$component = mountBottleCreateComponent($this->user);
|
||||
|
||||
// Valid name
|
||||
$component->set('form.name', 'Valid Bottle Name')
|
||||
->assertHasNoErrors(['form.name']);
|
||||
|
||||
// Empty name
|
||||
$component->set('form.name', '')
|
||||
->assertHasErrors(['form.name']);
|
||||
|
||||
// Name too long
|
||||
$component->set('form.name', str_repeat('a', 51))
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates bottle size field in real-time', function () {
|
||||
$component = mountBottleCreateComponent($this->user);
|
||||
|
||||
// Valid size
|
||||
$component->set('form.size', '100')
|
||||
->assertHasNoErrors(['form.size']);
|
||||
|
||||
// Empty size
|
||||
$component->set('form.size', '')
|
||||
->assertHasErrors(['form.size']);
|
||||
|
||||
// Negative size
|
||||
$component->set('form.size', '-50')
|
||||
->assertHasErrors(['form.size']);
|
||||
});
|
||||
|
||||
it('validates bottle price fields in real-time', function () {
|
||||
$component = mountBottleCreateComponent($this->user);
|
||||
|
||||
// Valid sale price
|
||||
$component->set('form.sale_price', '50000')
|
||||
->assertHasNoErrors(['form.sale_price']);
|
||||
|
||||
// Empty sale price
|
||||
$component->set('form.sale_price', '')
|
||||
->assertHasErrors(['form.sale_price']);
|
||||
|
||||
// Negative sale price
|
||||
$component->set('form.sale_price', '-50000')
|
||||
->assertHasErrors(['form.sale_price']);
|
||||
|
||||
// Valid cost price
|
||||
$component->set('form.cost_price', '30000')
|
||||
->assertHasNoErrors(['form.cost_price']);
|
||||
|
||||
// Negative cost price
|
||||
$component->set('form.cost_price', '-30000')
|
||||
->assertHasErrors(['form.cost_price']);
|
||||
});
|
||||
|
||||
it('validates bottle outlet_ids in real-time', function () {
|
||||
$component = mountBottleCreateComponent($this->user);
|
||||
|
||||
// Valid outlet IDs
|
||||
$component->set('form.name', 'Test Bottle')
|
||||
->set('form.size', '100')
|
||||
->set('form.sale_price', '50000')
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->assertHasNoErrors(['form.outlet_ids']);
|
||||
|
||||
// Empty outlet IDs - should trigger validation on submit
|
||||
$component->set('form.outlet_ids', [])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.outlet_ids']);
|
||||
|
||||
// Invalid outlet ID
|
||||
$component = mountBottleCreateComponent($this->user);
|
||||
$component->set('form.name', 'Test Bottle')
|
||||
->set('form.size', '100')
|
||||
->set('form.sale_price', '50000')
|
||||
->set('form.outlet_ids', [999])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.outlet_ids.*']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Integration Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('creates bottle and attaches to outlets correctly', function () {
|
||||
$outlet1 = $this->outlets->first();
|
||||
$outlet2 = $this->outlets->last();
|
||||
|
||||
mountBottleCreateComponent($this->user)
|
||||
->set('form.name', 'Integration Test Bottle')
|
||||
->set('form.size', '200')
|
||||
->set('form.cost_price', '75000') // Required for Owner role
|
||||
->set('form.sale_price', '100000')
|
||||
->set('form.outlet_ids', [$outlet1->id, $outlet2->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$bottle = Bottle::where('size', 200)->first();
|
||||
expect($bottle)->not->toBeNull();
|
||||
expect($bottle->outlets)->toHaveCount(2);
|
||||
expect($bottle->outlets->pluck('id')->toArray())->toContain($outlet1->id, $outlet2->id);
|
||||
});
|
||||
|
||||
it('handles large numbers in bottle price fields', function () {
|
||||
mountBottleCreateComponent($this->user)
|
||||
->set('form.name', 'Expensive Bottle')
|
||||
->set('form.size', '500')
|
||||
->set('form.cost_price', '5000000') // 5 million
|
||||
->set('form.sale_price', '7500000') // 7.5 million
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$bottle = Bottle::where('size', 500)->first();
|
||||
expect($bottle->cost_price)->toBe(5000000);
|
||||
expect($bottle->sale_price)->toBe(7500000);
|
||||
});
|
||||
@ -1,344 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Bottle;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
|
||||
$this->ownerRole = Role::where('name', 'Owner')->first();
|
||||
$this->user->roles()->attach($this->ownerRole->id);
|
||||
|
||||
// Create test outlets
|
||||
$this->outlets = Outlet::factory()->count(3)->create();
|
||||
});
|
||||
|
||||
function mountBottleEditComponent(User $user, Bottle $bottle)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(\App\Livewire\Studio\Catalog\Bottle\Edit::class, ['bottle' => $bottle->hash]);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Edit Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders bottle edit page successfully', function () {
|
||||
$bottle = Bottle::factory()->create();
|
||||
|
||||
mountBottleEditComponent($this->user, $bottle)
|
||||
->assertViewIs('livewire.studio.catalog.bottle.form')
|
||||
->assertViewHas('pageTitle', 'Ubah Botol');
|
||||
});
|
||||
|
||||
it('populates bottle form with bottle data on mount', function () {
|
||||
$bottle = Bottle::factory()->create([
|
||||
'name' => 'Test Bottle',
|
||||
'size' => 100,
|
||||
'cost_price' => 50000,
|
||||
'sale_price' => 75000,
|
||||
'description' => 'Test description',
|
||||
]);
|
||||
|
||||
$bottle->outlets()->attach([$this->outlets->first()->id]);
|
||||
|
||||
$component = mountBottleEditComponent($this->user, $bottle);
|
||||
|
||||
expect($component->form->name)->toBe('Test Bottle');
|
||||
expect($component->form->size)->toBe('100');
|
||||
expect($component->form->cost_price)->toBe('50000');
|
||||
expect($component->form->sale_price)->toBe('75000');
|
||||
expect($component->form->description)->toBe('Test description');
|
||||
expect($component->form->outlet_ids)->toBe([$this->outlets->first()->id]);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Update Bottle Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('updates bottle successfully when authorized', function () {
|
||||
$updatePermission = Permission::where('name', 'update bottle')->first();
|
||||
$this->user->givePermissionTo($updatePermission);
|
||||
|
||||
$bottle = Bottle::factory()->create([
|
||||
'name' => 'Original Bottle',
|
||||
'size' => 50,
|
||||
'cost_price' => 25000,
|
||||
'sale_price' => 50000,
|
||||
]);
|
||||
|
||||
$bottle->outlets()->attach([$this->outlets->first()->id]);
|
||||
|
||||
$updateData = [
|
||||
'name' => 'Updated Bottle',
|
||||
'size' => '100',
|
||||
'cost_price' => '40000',
|
||||
'sale_price' => '60000',
|
||||
'description' => 'Updated description',
|
||||
'outlet_ids' => [$this->outlets->first()->id, $this->outlets->last()->id],
|
||||
];
|
||||
|
||||
mountBottleEditComponent($this->user, $bottle)
|
||||
->set('form.name', $updateData['name'])
|
||||
->set('form.size', $updateData['size'])
|
||||
->set('form.cost_price', $updateData['cost_price'])
|
||||
->set('form.sale_price', $updateData['sale_price'])
|
||||
->set('form.description', $updateData['description'])
|
||||
->set('form.outlet_ids', $updateData['outlet_ids'])
|
||||
->call('save')
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect(route('studio.catalog.bottle.index'));
|
||||
|
||||
$bottle->refresh();
|
||||
expect($bottle->name)->toBe($updateData['name']);
|
||||
expect($bottle->size)->toBe(100);
|
||||
expect($bottle->cost_price)->toBe(40000);
|
||||
expect($bottle->sale_price)->toBe(60000);
|
||||
expect($bottle->description)->toBe($updateData['description']);
|
||||
expect($bottle->outlets)->toHaveCount(2);
|
||||
});
|
||||
|
||||
it('prevents bottle update when unauthorized', function () {
|
||||
$this->ownerRole->revokePermissionTo('update bottle');
|
||||
Gate::define('update bottle', fn () => false);
|
||||
|
||||
$bottle = Bottle::factory()->create();
|
||||
|
||||
mountBottleEditComponent($this->user, $bottle)
|
||||
->set('form.name', 'Updated Name')
|
||||
->call('save')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('syncs bottle outlet relationships correctly on update', function () {
|
||||
$updatePermission = Permission::where('name', 'update bottle')->first();
|
||||
$this->user->givePermissionTo($updatePermission);
|
||||
|
||||
$bottle = Bottle::factory()->create();
|
||||
$bottle->outlets()->attach([$this->outlets->first()->id]);
|
||||
|
||||
// Change to different outlets
|
||||
mountBottleEditComponent($this->user, $bottle)
|
||||
->set('form.name', 'Updated Bottle')
|
||||
->set('form.size', '200')
|
||||
->set('form.sale_price', '50000')
|
||||
->set('form.outlet_ids', [$this->outlets[1]->id, $this->outlets[2]->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$bottle->refresh();
|
||||
expect($bottle->outlets)->toHaveCount(2);
|
||||
expect($bottle->outlets->pluck('id')->toArray())->toBe([$this->outlets[1]->id, $this->outlets[2]->id]);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Form Validation Tests on Edit
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates required name field on bottle update', function () {
|
||||
$bottle = Bottle::factory()->create();
|
||||
|
||||
mountBottleEditComponent($this->user, $bottle)
|
||||
->set('form.name', '')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates name field max length on bottle update', function () {
|
||||
$bottle = Bottle::factory()->create();
|
||||
|
||||
mountBottleEditComponent($this->user, $bottle)
|
||||
->set('form.name', str_repeat('a', 51))
|
||||
->call('save')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates required size field on bottle update', function () {
|
||||
$bottle = Bottle::factory()->create();
|
||||
|
||||
mountBottleEditComponent($this->user, $bottle)
|
||||
->set('form.size', '')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.size']);
|
||||
});
|
||||
|
||||
it('validates size is unsigned integer on bottle update', function () {
|
||||
$bottle = Bottle::factory()->create();
|
||||
|
||||
mountBottleEditComponent($this->user, $bottle)
|
||||
->set('form.size', '-100')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.size']);
|
||||
});
|
||||
|
||||
it('validates required sale_price field on bottle update', function () {
|
||||
$bottle = Bottle::factory()->create();
|
||||
|
||||
mountBottleEditComponent($this->user, $bottle)
|
||||
->set('form.sale_price', '')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.sale_price']);
|
||||
});
|
||||
|
||||
it('validates sale_price is unsigned integer on bottle update', function () {
|
||||
$bottle = Bottle::factory()->create();
|
||||
|
||||
mountBottleEditComponent($this->user, $bottle)
|
||||
->set('form.sale_price', '-50000')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.sale_price']);
|
||||
});
|
||||
|
||||
it('validates required outlet_ids field on bottle update', function () {
|
||||
$bottle = Bottle::factory()->create();
|
||||
|
||||
mountBottleEditComponent($this->user, $bottle)
|
||||
->set('form.outlet_ids', [])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.outlet_ids']);
|
||||
});
|
||||
|
||||
it('validates outlet_ids contains valid outlet ids on bottle update', function () {
|
||||
$bottle = Bottle::factory()->create();
|
||||
|
||||
mountBottleEditComponent($this->user, $bottle)
|
||||
->set('form.outlet_ids', [999])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.outlet_ids.*']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Real-time Validation Tests on Edit
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates bottle name field in real-time on edit', function () {
|
||||
$bottle = Bottle::factory()->create();
|
||||
|
||||
$component = mountBottleEditComponent($this->user, $bottle);
|
||||
|
||||
// Valid name
|
||||
$component->set('form.name', 'Valid Updated Bottle Name')
|
||||
->assertHasNoErrors(['form.name']);
|
||||
|
||||
// Empty name
|
||||
$component->set('form.name', '')
|
||||
->assertHasErrors(['form.name']);
|
||||
|
||||
// Name too long
|
||||
$component->set('form.name', str_repeat('a', 51))
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates bottle size field in real-time on edit', function () {
|
||||
$bottle = Bottle::factory()->create();
|
||||
|
||||
$component = mountBottleEditComponent($this->user, $bottle);
|
||||
|
||||
// Valid size
|
||||
$component->set('form.size', '150')
|
||||
->assertHasNoErrors(['form.size']);
|
||||
|
||||
// Empty size
|
||||
$component->set('form.size', '')
|
||||
->assertHasErrors(['form.size']);
|
||||
|
||||
// Negative size
|
||||
$component->set('form.size', '-50')
|
||||
->assertHasErrors(['form.size']);
|
||||
});
|
||||
|
||||
it('validates bottle price fields in real-time on edit', function () {
|
||||
$bottle = Bottle::factory()->create();
|
||||
|
||||
$component = mountBottleEditComponent($this->user, $bottle);
|
||||
|
||||
// Valid sale price
|
||||
$component->set('form.sale_price', '75000')
|
||||
->assertHasNoErrors(['form.sale_price']);
|
||||
|
||||
// Empty sale price
|
||||
$component->set('form.sale_price', '')
|
||||
->assertHasErrors(['form.sale_price']);
|
||||
|
||||
// Negative sale price
|
||||
$component->set('form.sale_price', '-75000')
|
||||
->assertHasErrors(['form.sale_price']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Integration Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('updates bottle and syncs outlet relationships', function () {
|
||||
$updatePermission = Permission::where('name', 'update bottle')->first();
|
||||
$this->user->givePermissionTo($updatePermission);
|
||||
|
||||
$bottle = Bottle::factory()->create();
|
||||
$bottle->outlets()->attach([$this->outlets->first()->id]);
|
||||
|
||||
$newOutlets = [$this->outlets[1]->id, $this->outlets[2]->id];
|
||||
|
||||
mountBottleEditComponent($this->user, $bottle)
|
||||
->set('form.name', 'Updated Integration Bottle')
|
||||
->set('form.size', '300')
|
||||
->set('form.sale_price', '90000')
|
||||
->set('form.outlet_ids', $newOutlets)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$bottle->refresh();
|
||||
expect($bottle->name)->toBe('Updated Integration Bottle');
|
||||
expect($bottle->size)->toBe(300);
|
||||
expect($bottle->outlets)->toHaveCount(2);
|
||||
expect($bottle->outlets->pluck('id')->toArray())->toBe($newOutlets);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Error Handling and Edge Cases
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('handles invalid bottle hash in edit component', function () {
|
||||
expect(fn () => mountBottleEditComponent($this->user, 'invalid-hash'))
|
||||
->toThrow(\TypeError::class);
|
||||
});
|
||||
|
||||
it('validates bottle form data integrity during operations', function () {
|
||||
// Test that form data is properly reset between operations
|
||||
$bottle1 = Bottle::factory()->create(['name' => 'First Bottle']);
|
||||
$bottle2 = Bottle::factory()->create(['name' => 'Second Bottle']);
|
||||
|
||||
$component = mountBottleEditComponent($this->user, $bottle1);
|
||||
|
||||
// Open modal for first bottle
|
||||
expect($component->form->name)->toBe('First Bottle');
|
||||
|
||||
// Verify first bottle wasn't modified
|
||||
$bottle1->refresh();
|
||||
expect($bottle1->name)->toBe('First Bottle');
|
||||
});
|
||||
@ -1,115 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Bottle;
|
||||
use App\Models\Employee;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
|
||||
$this->ownerRole = Role::where('name', 'Owner')->first();
|
||||
$this->user->roles()->attach($this->ownerRole->id);
|
||||
});
|
||||
|
||||
function mountBottleIndexComponent(User $user)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(\App\Livewire\Studio\Catalog\Bottle\Index::class);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Index Component Rendering & Basic Functionality
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders bottle index page successfully', function () {
|
||||
mountBottleIndexComponent($this->user)
|
||||
->assertViewIs('livewire.studio.catalog.bottle.index')
|
||||
->assertViewHas('pageTitle', 'Botol');
|
||||
});
|
||||
|
||||
it('displays bottles correctly', function () {
|
||||
Bottle::factory()->count(3)->create();
|
||||
|
||||
// Just check that the component renders without errors and has bottles
|
||||
$component = mountBottleIndexComponent($this->user);
|
||||
$component->assertViewIs('livewire.studio.catalog.bottle.index');
|
||||
expect(Bottle::count())->toBe(3);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Delete Bottle Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('deletes bottle successfully when authorized', function () {
|
||||
$bottle = Bottle::factory()->create([
|
||||
'name' => 'Bottle to Delete',
|
||||
]);
|
||||
|
||||
mountBottleIndexComponent($this->user)
|
||||
->call('delete', $bottle)
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
$this->assertSoftDeleted('bottles', [
|
||||
'id' => $bottle->id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('prevents bottle deletion when unauthorized', function () {
|
||||
$this->ownerRole->revokePermissionTo('delete bottle');
|
||||
|
||||
$bottle = Bottle::factory()->create();
|
||||
|
||||
mountBottleIndexComponent($this->user)
|
||||
->call('delete', $bottle)
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authorization Edge Cases
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('handles unauthorized user with revoked permissions', function () {
|
||||
// Revoke all bottle permissions
|
||||
$this->ownerRole->revokePermissionTo(['delete bottle']);
|
||||
|
||||
$bottle = Bottle::factory()->create();
|
||||
|
||||
// Test delete
|
||||
mountBottleIndexComponent($this->user)
|
||||
->call('delete', $bottle)
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('handles user with partial permissions correctly', function () {
|
||||
// Remove owner role and create a user with limited permissions
|
||||
$this->user->roles()->detach();
|
||||
$limitedRole = Role::create(['name' => 'Limited User']);
|
||||
$this->user->roles()->attach($limitedRole->id);
|
||||
|
||||
// Give only some permissions but not delete
|
||||
$limitedRole->givePermissionTo([]);
|
||||
|
||||
$bottle = Bottle::factory()->create();
|
||||
|
||||
// Should not be able to delete
|
||||
mountBottleIndexComponent($this->user)
|
||||
->call('delete', $bottle)
|
||||
->assertForbidden();
|
||||
});
|
||||
@ -1,573 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Studio\Catalog\Brand;
|
||||
use App\Models\Brand as BrandModel;
|
||||
use App\Models\Employee;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
|
||||
$this->ownerRole = Role::where('name', 'Owner')->first();
|
||||
$this->user->roles()->attach($this->ownerRole->id);
|
||||
});
|
||||
|
||||
function mountBrandComponent(User $user)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(Brand::class);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders page successfully', function () {
|
||||
mountBrandComponent($this->user)
|
||||
->assertViewIs('livewire.studio.catalog.brands')
|
||||
->assertViewHas('pageTitle', 'Merek');
|
||||
});
|
||||
|
||||
it('initializes form properties on mount', function () {
|
||||
$component = mountBrandComponent($this->user);
|
||||
|
||||
expect($component->method)->toBe('create');
|
||||
expect($component->modalTitle)->toBe('');
|
||||
expect($component->form->name)->toBe('');
|
||||
expect($component->form->image)->toBe([]);
|
||||
expect($component->form->brand)->toBeNull();
|
||||
});
|
||||
|
||||
it('displays all brands', function () {
|
||||
BrandModel::factory()->count(10)->create();
|
||||
|
||||
$brand = BrandModel::first();
|
||||
|
||||
mountBrandComponent($this->user)
|
||||
->assertSee($brand->name);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Open Modal Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('opens modal for create mode', function () {
|
||||
$component = mountBrandComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Merek');
|
||||
|
||||
expect($component->method)->toBe('create');
|
||||
expect($component->modalTitle)->toBe('Tambah Merek');
|
||||
expect($component->form->brand)->toBeNull();
|
||||
});
|
||||
|
||||
it('opens modal for update mode with brand data', function () {
|
||||
$brand = BrandModel::factory()->create([
|
||||
'name' => 'Test Brand',
|
||||
]);
|
||||
|
||||
$component = mountBrandComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Merek', $brand->hash);
|
||||
|
||||
expect($component->method)->toBe('update');
|
||||
expect($component->modalTitle)->toBe('Edit Merek');
|
||||
expect($component->form->brand->id)->toBe($brand->id);
|
||||
expect($component->form->name)->toBe('Test Brand');
|
||||
});
|
||||
|
||||
it('resets validation and error bag when opening modal', function () {
|
||||
$component = mountBrandComponent($this->user)
|
||||
->set('form.name', '')
|
||||
->call('create')
|
||||
->assertHasErrors(['form.name']);
|
||||
|
||||
$component->call('openModal', 'create', 'Tambah Merek')
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Create Brand Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('prevents create when user is unauthorized', function () {
|
||||
$this->ownerRole->revokePermissionTo('create brand');
|
||||
Gate::define('create brand', fn () => false);
|
||||
|
||||
mountBrandComponent($this->user)
|
||||
->call('create')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('creates brand successfully when authorized', function () {
|
||||
$data = [
|
||||
'name' => 'New Brand',
|
||||
];
|
||||
|
||||
mountBrandComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Merek')
|
||||
->set('form.name', $data['name'])
|
||||
->call('create')
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
expect(BrandModel::count())->toBe(1);
|
||||
$brand = BrandModel::first();
|
||||
expect($brand->name)->toBe($data['name']);
|
||||
expect($brand->sort_order)->toBe(1);
|
||||
});
|
||||
|
||||
it('creates brand with auto-incremented sort_order', function () {
|
||||
BrandModel::factory()->count(3)->create();
|
||||
|
||||
mountBrandComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Merek')
|
||||
->set('form.name', 'Fourth Brand')
|
||||
->call('create')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$newBrand = BrandModel::where('name', 'Fourth Brand')->first();
|
||||
expect($newBrand->sort_order)->toBe(4);
|
||||
});
|
||||
|
||||
it('creates brand with empty image array', function () {
|
||||
mountBrandComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Merek')
|
||||
->set('form.name', 'Brand Without Image')
|
||||
->set('form.image', [])
|
||||
->call('create')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$brand = BrandModel::first();
|
||||
expect($brand->name)->toBe('Brand Without Image');
|
||||
});
|
||||
|
||||
it('creates brand and generates slug automatically', function () {
|
||||
mountBrandComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Merek')
|
||||
->set('form.name', 'Test Brand Name')
|
||||
->call('create')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$brand = BrandModel::first();
|
||||
expect($brand->slug)->not->toBeNull();
|
||||
expect($brand->slug)->toBeString();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Update Brand Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('prevents update when user is unauthorized', function () {
|
||||
$this->ownerRole->revokePermissionTo('update brand');
|
||||
|
||||
$brand = BrandModel::factory()->create();
|
||||
|
||||
Gate::define('update brand', fn () => false);
|
||||
|
||||
mountBrandComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Merek', $brand->hash)
|
||||
->set('form.name', 'Updated Name')
|
||||
->call('update')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('updates brand successfully when authorized', function () {
|
||||
$brand = BrandModel::factory()->create([
|
||||
'name' => 'Original Name',
|
||||
]);
|
||||
|
||||
mountBrandComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Merek', $brand->hash)
|
||||
->set('form.name', 'Updated Name')
|
||||
->call('update')
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
$brand->refresh();
|
||||
expect($brand->name)->toBe('Updated Name');
|
||||
});
|
||||
|
||||
it('updates brand without changing sort_order', function () {
|
||||
$brand = BrandModel::factory()->create([
|
||||
'name' => 'Original Name',
|
||||
'sort_order' => 5,
|
||||
]);
|
||||
|
||||
$originalSortOrder = $brand->sort_order;
|
||||
|
||||
mountBrandComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Merek', $brand->hash)
|
||||
->set('form.name', 'Updated Name')
|
||||
->call('update')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$brand->refresh();
|
||||
expect($brand->name)->toBe('Updated Name');
|
||||
expect($brand->sort_order)->toBe($originalSortOrder);
|
||||
});
|
||||
|
||||
it('updates brand with empty image array', function () {
|
||||
$brand = BrandModel::factory()->create([
|
||||
'name' => 'Test Brand',
|
||||
]);
|
||||
|
||||
mountBrandComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Merek', $brand->hash)
|
||||
->set('form.image', [])
|
||||
->call('update')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$brand->refresh();
|
||||
expect($brand->name)->toBe('Test Brand');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Delete Brand Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('deletes brand successfully', function () {
|
||||
$brand = BrandModel::factory()->create([
|
||||
'name' => 'Brand To Delete',
|
||||
]);
|
||||
|
||||
mountBrandComponent($this->user)
|
||||
->call('delete', $brand)
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
expect(BrandModel::count())->toBe(0);
|
||||
$this->assertSoftDeleted('brands', [
|
||||
'id' => $brand->id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('adjusts sort_order of remaining brands after deletion', function () {
|
||||
$brand1 = BrandModel::factory()->create(['sort_order' => 1, 'name' => 'First']);
|
||||
$brand2 = BrandModel::factory()->create(['sort_order' => 2, 'name' => 'Second']);
|
||||
$brand3 = BrandModel::factory()->create(['sort_order' => 3, 'name' => 'Third']);
|
||||
|
||||
mountBrandComponent($this->user)
|
||||
->call('delete', $brand2);
|
||||
|
||||
$brand1->refresh();
|
||||
$brand3->refresh();
|
||||
|
||||
expect($brand1->sort_order)->toBe(1);
|
||||
expect($brand3->sort_order)->toBe(2);
|
||||
});
|
||||
|
||||
it('does not adjust sort_order when deleting last brand', function () {
|
||||
$brand = BrandModel::factory()->create(['sort_order' => 1]);
|
||||
|
||||
mountBrandComponent($this->user)
|
||||
->call('delete', $brand);
|
||||
|
||||
expect(BrandModel::count())->toBe(0);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Sort Order Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('prevents sortOrder when user is unauthorized', function () {
|
||||
$this->ownerRole->revokePermissionTo('update brand');
|
||||
|
||||
$brand = BrandModel::factory()->create(['sort_order' => 2]);
|
||||
|
||||
Gate::define('update brand', fn () => false);
|
||||
|
||||
mountBrandComponent($this->user)
|
||||
->call('sortOrder', $brand, 'up')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('moves brand up when authorized', function () {
|
||||
$brand1 = BrandModel::factory()->create(['sort_order' => 1, 'name' => 'First']);
|
||||
$brand2 = BrandModel::factory()->create(['sort_order' => 2, 'name' => 'Second']);
|
||||
|
||||
mountBrandComponent($this->user)
|
||||
->call('sortOrder', $brand2, 'up')
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
$brand1->refresh();
|
||||
$brand2->refresh();
|
||||
|
||||
expect($brand1->sort_order)->toBe(2);
|
||||
expect($brand2->sort_order)->toBe(1);
|
||||
});
|
||||
|
||||
it('moves brand down when authorized', function () {
|
||||
$brand1 = BrandModel::factory()->create(['sort_order' => 1, 'name' => 'First']);
|
||||
$brand2 = BrandModel::factory()->create(['sort_order' => 2, 'name' => 'Second']);
|
||||
|
||||
mountBrandComponent($this->user)
|
||||
->call('sortOrder', $brand1, 'down')
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
$brand1->refresh();
|
||||
$brand2->refresh();
|
||||
|
||||
expect($brand1->sort_order)->toBe(2);
|
||||
expect($brand2->sort_order)->toBe(1);
|
||||
});
|
||||
|
||||
it('does not move brand up when it is already first', function () {
|
||||
$brand1 = BrandModel::factory()->create(['sort_order' => 1, 'name' => 'First']);
|
||||
$brand2 = BrandModel::factory()->create(['sort_order' => 2, 'name' => 'Second']);
|
||||
|
||||
$originalOrder1 = $brand1->sort_order;
|
||||
$originalOrder2 = $brand2->sort_order;
|
||||
|
||||
mountBrandComponent($this->user)
|
||||
->call('sortOrder', $brand1, 'up');
|
||||
|
||||
$brand1->refresh();
|
||||
$brand2->refresh();
|
||||
|
||||
expect($brand1->sort_order)->toBe($originalOrder1);
|
||||
expect($brand2->sort_order)->toBe($originalOrder2);
|
||||
});
|
||||
|
||||
it('does not move brand down when it is already last', function () {
|
||||
$brand1 = BrandModel::factory()->create(['sort_order' => 1, 'name' => 'First']);
|
||||
$brand2 = BrandModel::factory()->create(['sort_order' => 2, 'name' => 'Second']);
|
||||
|
||||
$originalOrder1 = $brand1->sort_order;
|
||||
$originalOrder2 = $brand2->sort_order;
|
||||
|
||||
mountBrandComponent($this->user)
|
||||
->call('sortOrder', $brand2, 'down');
|
||||
|
||||
$brand1->refresh();
|
||||
$brand2->refresh();
|
||||
|
||||
expect($brand1->sort_order)->toBe($originalOrder1);
|
||||
expect($brand2->sort_order)->toBe($originalOrder2);
|
||||
});
|
||||
|
||||
it('moves brand to first position', function () {
|
||||
$brand1 = BrandModel::factory()->create(['sort_order' => 1, 'name' => 'First']);
|
||||
$brand2 = BrandModel::factory()->create(['sort_order' => 2, 'name' => 'Second']);
|
||||
$brand3 = BrandModel::factory()->create(['sort_order' => 3, 'name' => 'Third']);
|
||||
|
||||
mountBrandComponent($this->user)
|
||||
->call('sortOrder', $brand3, 'first')
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
$brand1->refresh();
|
||||
$brand2->refresh();
|
||||
$brand3->refresh();
|
||||
|
||||
expect($brand3->sort_order)->toBe(1);
|
||||
expect($brand1->sort_order)->toBe(2);
|
||||
expect($brand2->sort_order)->toBe(3);
|
||||
});
|
||||
|
||||
it('moves brand to last position', function () {
|
||||
$brand1 = BrandModel::factory()->create(['sort_order' => 1, 'name' => 'First']);
|
||||
$brand2 = BrandModel::factory()->create(['sort_order' => 2, 'name' => 'Second']);
|
||||
$brand3 = BrandModel::factory()->create(['sort_order' => 3, 'name' => 'Third']);
|
||||
|
||||
mountBrandComponent($this->user)
|
||||
->call('sortOrder', $brand1, 'last')
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
$brand1->refresh();
|
||||
$brand2->refresh();
|
||||
$brand3->refresh();
|
||||
|
||||
expect($brand1->sort_order)->toBe(3);
|
||||
expect($brand2->sort_order)->toBe(1);
|
||||
expect($brand3->sort_order)->toBe(2);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Validation Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates required name field on create', function () {
|
||||
mountBrandComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Merek')
|
||||
->set('form.name', '')
|
||||
->call('create')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates name field max length', function () {
|
||||
mountBrandComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Merek')
|
||||
->set('form.name', str_repeat('a', 21))
|
||||
->call('create')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates unique name on create', function () {
|
||||
$existingBrand = BrandModel::factory()->create(['name' => 'Existing Brand']);
|
||||
|
||||
mountBrandComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Merek')
|
||||
->set('form.name', 'Existing Brand')
|
||||
->call('create')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('allows same name on update of same brand', function () {
|
||||
$brand = BrandModel::factory()->create(['name' => 'Original Name']);
|
||||
|
||||
mountBrandComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Merek', $brand->hash)
|
||||
->set('form.name', 'Original Name')
|
||||
->call('update')
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
|
||||
it('validates unique name on update with different brand name', function () {
|
||||
$brand1 = BrandModel::factory()->create(['name' => 'Brand One']);
|
||||
$brand2 = BrandModel::factory()->create(['name' => 'Brand Two']);
|
||||
|
||||
mountBrandComponent($this->user)
|
||||
->dispatch('modal:open', 'update', 'Edit Merek', $brand1->hash)
|
||||
->set('form.name', 'Brand Two')
|
||||
->call('update')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates name uniqueness ignores soft deleted brands', function () {
|
||||
$deletedBrand = BrandModel::factory()->create(['name' => 'Deleted Brand']);
|
||||
$deletedBrand->delete();
|
||||
|
||||
mountBrandComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Merek')
|
||||
->set('form.name', 'Deleted Brand')
|
||||
->call('create')
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
|
||||
it('validates image is nullable', function () {
|
||||
mountBrandComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Merek')
|
||||
->set('form.name', 'Test Brand')
|
||||
->set('form.image', null)
|
||||
->call('create')
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Integration Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('maintains data integrity after multiple operations', function () {
|
||||
$createPermission = Permission::where('name', 'create brand')->first();
|
||||
$updatePermission = Permission::where('name', 'update brand')->first();
|
||||
$this->user->givePermissionTo([$createPermission, $updatePermission]);
|
||||
|
||||
// Create brands
|
||||
mountBrandComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Merek')
|
||||
->set('form.name', 'Brand One')
|
||||
->call('create');
|
||||
|
||||
mountBrandComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Merek')
|
||||
->set('form.name', 'Brand Two')
|
||||
->call('create');
|
||||
|
||||
mountBrandComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Merek')
|
||||
->set('form.name', 'Brand Three')
|
||||
->call('create');
|
||||
|
||||
// Update brand
|
||||
$brand2 = BrandModel::where('name', 'Brand Two')->first();
|
||||
mountBrandComponent($this->user)
|
||||
->dispatch('modal:open', 'update', 'Edit Merek', $brand2->hash)
|
||||
->set('form.name', 'Brand Two Updated')
|
||||
->call('update');
|
||||
|
||||
// Delete brand
|
||||
$brand1 = BrandModel::where('name', 'Brand One')->first();
|
||||
mountBrandComponent($this->user)
|
||||
->call('delete', $brand1);
|
||||
|
||||
expect(BrandModel::count())->toBe(2);
|
||||
$remainingBrands = BrandModel::orderBy('sort_order')->get();
|
||||
expect($remainingBrands[0]->name)->toBe('Brand Two Updated');
|
||||
expect($remainingBrands[0]->sort_order)->toBe(1);
|
||||
expect($remainingBrands[1]->name)->toBe('Brand Three');
|
||||
expect($remainingBrands[1]->sort_order)->toBe(2);
|
||||
});
|
||||
|
||||
it('handles sort order operations correctly with multiple brands', function () {
|
||||
$brand1 = BrandModel::factory()->create(['sort_order' => 1, 'name' => 'First']);
|
||||
$brand2 = BrandModel::factory()->create(['sort_order' => 2, 'name' => 'Second']);
|
||||
$brand3 = BrandModel::factory()->create(['sort_order' => 3, 'name' => 'Third']);
|
||||
$brand4 = BrandModel::factory()->create(['sort_order' => 4, 'name' => 'Fourth']);
|
||||
|
||||
// Move brand3 to first
|
||||
mountBrandComponent($this->user)
|
||||
->call('sortOrder', $brand3, 'first');
|
||||
|
||||
$brand1->refresh();
|
||||
$brand2->refresh();
|
||||
$brand3->refresh();
|
||||
$brand4->refresh();
|
||||
|
||||
expect($brand3->sort_order)->toBe(1);
|
||||
expect($brand1->sort_order)->toBe(2);
|
||||
expect($brand2->sort_order)->toBe(3);
|
||||
expect($brand4->sort_order)->toBe(4);
|
||||
|
||||
// Move brand2 to last
|
||||
mountBrandComponent($this->user)
|
||||
->call('sortOrder', $brand2, 'last');
|
||||
|
||||
$brand1->refresh();
|
||||
$brand2->refresh();
|
||||
$brand3->refresh();
|
||||
$brand4->refresh();
|
||||
|
||||
expect($brand3->sort_order)->toBe(1);
|
||||
expect($brand1->sort_order)->toBe(2);
|
||||
expect($brand4->sort_order)->toBe(3);
|
||||
expect($brand2->sort_order)->toBe(4);
|
||||
});
|
||||
|
||||
it('creates brand with correct slug generation', function () {
|
||||
mountBrandComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Merek')
|
||||
->set('form.name', 'Yves Saint Laurent')
|
||||
->call('create')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$brand = BrandModel::first();
|
||||
expect($brand->slug)->not->toBeNull();
|
||||
expect($brand->slug)->toBeString();
|
||||
expect(strlen($brand->slug))->toBeLessThanOrEqual(30);
|
||||
});
|
||||
@ -1,925 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Studio\Catalog\Category;
|
||||
use App\Models\Category as CategoryModel;
|
||||
use App\Models\Employee;
|
||||
use App\Models\PushNotification;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
|
||||
$this->ownerRole = Role::where('name', 'Owner')->first();
|
||||
$this->user->roles()->attach($this->ownerRole->id);
|
||||
});
|
||||
|
||||
function mountCategoryComponent(User $user)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(Category::class);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders page successfully', function () {
|
||||
mountCategoryComponent($this->user)
|
||||
->assertViewIs('livewire.studio.catalog.categories')
|
||||
->assertViewHas('pageTitle', 'Kategori');
|
||||
});
|
||||
|
||||
it('initializes form properties on mount', function () {
|
||||
$component = mountCategoryComponent($this->user);
|
||||
|
||||
expect($component->method)->toBe('create');
|
||||
expect($component->modalTitle)->toBe('');
|
||||
expect($component->form->name)->toBe('');
|
||||
expect($component->form->description)->toBeNull();
|
||||
expect($component->form->category)->toBeNull();
|
||||
});
|
||||
|
||||
it('displays all categories', function () {
|
||||
CategoryModel::factory()->count(10)->create();
|
||||
|
||||
$category = CategoryModel::first();
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->assertSee($category->name);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Open Modal Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('opens modal for create mode', function () {
|
||||
$component = mountCategoryComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Kategori');
|
||||
|
||||
expect($component->method)->toBe('create');
|
||||
expect($component->modalTitle)->toBe('Tambah Kategori');
|
||||
expect($component->form->category)->toBeNull();
|
||||
});
|
||||
|
||||
it('opens modal for update mode with category data', function () {
|
||||
$category = CategoryModel::factory()->create([
|
||||
'name' => 'Test Category',
|
||||
'description' => 'Test Description',
|
||||
]);
|
||||
|
||||
$component = mountCategoryComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Kategori', $category->hash);
|
||||
|
||||
expect($component->method)->toBe('update');
|
||||
expect($component->modalTitle)->toBe('Edit Kategori');
|
||||
expect($component->form->category->id)->toBe($category->id);
|
||||
expect($component->form->name)->toBe('Test Category');
|
||||
expect($component->form->description)->toBe('Test Description');
|
||||
});
|
||||
|
||||
it('resets validation and error bag when opening modal', function () {
|
||||
$component = mountCategoryComponent($this->user)
|
||||
->set('form.name', '')
|
||||
->call('create')
|
||||
->assertHasErrors(['form.name']);
|
||||
|
||||
$component->call('openModal', 'create', 'Tambah Kategori')
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Create Category Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('prevents create when user is unauthorized', function () {
|
||||
$this->ownerRole->revokePermissionTo('create category');
|
||||
Gate::define('create category', fn () => false);
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('create')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('creates category successfully when authorized', function () {
|
||||
$data = [
|
||||
'name' => 'New Category',
|
||||
'description' => 'Category description',
|
||||
];
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Kategori')
|
||||
->set('form.name', $data['name'])
|
||||
->set('form.description', $data['description'])
|
||||
->call('create')
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
expect(CategoryModel::count())->toBe(1);
|
||||
$category = CategoryModel::first();
|
||||
expect($category->name)->toBe($data['name']);
|
||||
expect($category->description)->toBe($data['description']);
|
||||
expect($category->sort_order)->toBe(1);
|
||||
});
|
||||
|
||||
it('creates category with sort_order as first position', function () {
|
||||
CategoryModel::factory()->count(3)->create();
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Kategori')
|
||||
->set('form.name', 'Fourth Category')
|
||||
->call('create')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$newCategory = CategoryModel::where('name', 'Fourth Category')->first();
|
||||
expect($newCategory->sort_order)->toBe(1);
|
||||
});
|
||||
|
||||
it('creates category with null description', function () {
|
||||
mountCategoryComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Kategori')
|
||||
->set('form.name', 'Test Category')
|
||||
->set('form.description', null)
|
||||
->call('create')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$category = CategoryModel::first();
|
||||
expect($category->name)->toBe('Test Category');
|
||||
expect($category->description)->toBeNull();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Update Category Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('prevents update when user is unauthorized', function () {
|
||||
$this->ownerRole->revokePermissionTo('update category');
|
||||
|
||||
$category = CategoryModel::factory()->create();
|
||||
|
||||
Gate::define('update category', fn () => false);
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Kategori', $category->hash)
|
||||
->set('form.name', 'Updated Name')
|
||||
->call('update')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('updates category successfully when authorized', function () {
|
||||
$permission = Permission::where('name', 'update category')->first();
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$category = CategoryModel::factory()->create([
|
||||
'name' => 'Original Name',
|
||||
'description' => 'Original Description',
|
||||
]);
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Kategori', $category->hash)
|
||||
->set('form.name', 'Updated Name')
|
||||
->set('form.description', 'Updated Description')
|
||||
->call('update')
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
$category->refresh();
|
||||
expect($category->name)->toBe('Updated Name');
|
||||
expect($category->description)->toBe('Updated Description');
|
||||
});
|
||||
|
||||
it('updates category without changing sort_order', function () {
|
||||
$permission = Permission::where('name', 'update category')->first();
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$category = CategoryModel::factory()->create([
|
||||
'name' => 'Original Name',
|
||||
'sort_order' => 5,
|
||||
]);
|
||||
|
||||
$originalSortOrder = $category->sort_order;
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Kategori', $category->hash)
|
||||
->set('form.name', 'Updated Name')
|
||||
->call('update')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$category->refresh();
|
||||
expect($category->name)->toBe('Updated Name');
|
||||
expect($category->sort_order)->toBe($originalSortOrder);
|
||||
});
|
||||
|
||||
it('updates category description to null', function () {
|
||||
$permission = Permission::where('name', 'update category')->first();
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$category = CategoryModel::factory()->create([
|
||||
'name' => 'Test Category',
|
||||
'description' => 'Has Description',
|
||||
]);
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Kategori', $category->hash)
|
||||
->set('form.description', null)
|
||||
->call('update')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$category->refresh();
|
||||
expect($category->description)->toBeNull();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Delete Category Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('deletes category successfully', function () {
|
||||
$category = CategoryModel::factory()->create([
|
||||
'name' => 'Category To Delete',
|
||||
]);
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('delete', $category)
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
expect(CategoryModel::count())->toBe(0);
|
||||
$this->assertSoftDeleted('categories', [
|
||||
'id' => $category->id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('adjusts sort_order of remaining categories after deletion', function () {
|
||||
$category1 = CategoryModel::factory()->create(['sort_order' => 1, 'name' => 'First']);
|
||||
$category2 = CategoryModel::factory()->create(['sort_order' => 2, 'name' => 'Second']);
|
||||
$category3 = CategoryModel::factory()->create(['sort_order' => 3, 'name' => 'Third']);
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('delete', $category2);
|
||||
|
||||
$category1->refresh();
|
||||
$category3->refresh();
|
||||
|
||||
expect($category1->sort_order)->toBe(1);
|
||||
expect($category3->sort_order)->toBe(2);
|
||||
});
|
||||
|
||||
it('does not adjust sort_order when deleting last category', function () {
|
||||
$category = CategoryModel::factory()->create(['sort_order' => 1]);
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('delete', $category);
|
||||
|
||||
expect(CategoryModel::count())->toBe(0);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Sort Order Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('prevents sortOrder when user is unauthorized', function () {
|
||||
$this->ownerRole->revokePermissionTo('update category');
|
||||
|
||||
$category = CategoryModel::factory()->create(['sort_order' => 2]);
|
||||
|
||||
Gate::define('update category', fn () => false);
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('sortOrder', $category, 'up')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('moves category up when authorized', function () {
|
||||
$permission = Permission::where('name', 'update category')->first();
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$category1 = CategoryModel::factory()->create(['sort_order' => 1, 'name' => 'First']);
|
||||
$category2 = CategoryModel::factory()->create(['sort_order' => 2, 'name' => 'Second']);
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('sortOrder', $category2, 'up')
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
$category1->refresh();
|
||||
$category2->refresh();
|
||||
|
||||
expect($category1->sort_order)->toBe(2);
|
||||
expect($category2->sort_order)->toBe(1);
|
||||
});
|
||||
|
||||
it('moves category down when authorized', function () {
|
||||
$permission = Permission::where('name', 'update category')->first();
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$category1 = CategoryModel::factory()->create(['sort_order' => 1, 'name' => 'First']);
|
||||
$category2 = CategoryModel::factory()->create(['sort_order' => 2, 'name' => 'Second']);
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('sortOrder', $category1, 'down')
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
$category1->refresh();
|
||||
$category2->refresh();
|
||||
|
||||
expect($category1->sort_order)->toBe(2);
|
||||
expect($category2->sort_order)->toBe(1);
|
||||
});
|
||||
|
||||
it('does not move category up when it is already first', function () {
|
||||
$permission = Permission::where('name', 'update category')->first();
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$category1 = CategoryModel::factory()->create(['sort_order' => 1, 'name' => 'First']);
|
||||
$category2 = CategoryModel::factory()->create(['sort_order' => 2, 'name' => 'Second']);
|
||||
|
||||
$originalOrder1 = $category1->sort_order;
|
||||
$originalOrder2 = $category2->sort_order;
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('sortOrder', $category1, 'up');
|
||||
|
||||
$category1->refresh();
|
||||
$category2->refresh();
|
||||
|
||||
expect($category1->sort_order)->toBe($originalOrder1);
|
||||
expect($category2->sort_order)->toBe($originalOrder2);
|
||||
});
|
||||
|
||||
it('does not move category down when it is already last', function () {
|
||||
$permission = Permission::where('name', 'update category')->first();
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$category1 = CategoryModel::factory()->create(['sort_order' => 1, 'name' => 'First']);
|
||||
$category2 = CategoryModel::factory()->create(['sort_order' => 2, 'name' => 'Second']);
|
||||
|
||||
$originalOrder1 = $category1->sort_order;
|
||||
$originalOrder2 = $category2->sort_order;
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('sortOrder', $category2, 'down');
|
||||
|
||||
$category1->refresh();
|
||||
$category2->refresh();
|
||||
|
||||
expect($category1->sort_order)->toBe($originalOrder1);
|
||||
expect($category2->sort_order)->toBe($originalOrder2);
|
||||
});
|
||||
|
||||
it('moves category to first position', function () {
|
||||
$permission = Permission::where('name', 'update category')->first();
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$category1 = CategoryModel::factory()->create(['sort_order' => 1, 'name' => 'First']);
|
||||
$category2 = CategoryModel::factory()->create(['sort_order' => 2, 'name' => 'Second']);
|
||||
$category3 = CategoryModel::factory()->create(['sort_order' => 3, 'name' => 'Third']);
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('sortOrder', $category3, 'first')
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
$category1->refresh();
|
||||
$category2->refresh();
|
||||
$category3->refresh();
|
||||
|
||||
expect($category3->sort_order)->toBe(1);
|
||||
expect($category1->sort_order)->toBe(2);
|
||||
expect($category2->sort_order)->toBe(3);
|
||||
});
|
||||
|
||||
it('moves category to last position', function () {
|
||||
$permission = Permission::where('name', 'update category')->first();
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$category1 = CategoryModel::factory()->create(['sort_order' => 1, 'name' => 'First']);
|
||||
$category2 = CategoryModel::factory()->create(['sort_order' => 2, 'name' => 'Second']);
|
||||
$category3 = CategoryModel::factory()->create(['sort_order' => 3, 'name' => 'Third']);
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('sortOrder', $category1, 'last')
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
$category1->refresh();
|
||||
$category2->refresh();
|
||||
$category3->refresh();
|
||||
|
||||
expect($category1->sort_order)->toBe(3);
|
||||
expect($category2->sort_order)->toBe(1);
|
||||
expect($category3->sort_order)->toBe(2);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Validation Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates required name field on create', function () {
|
||||
mountCategoryComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Kategori')
|
||||
->set('form.name', '')
|
||||
->call('create')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates name field max length', function () {
|
||||
mountCategoryComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Kategori')
|
||||
->set('form.name', str_repeat('a', 21))
|
||||
->call('create')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates unique name on create', function () {
|
||||
$existingCategory = CategoryModel::factory()->create(['name' => 'Existing Category']);
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Kategori')
|
||||
->set('form.name', 'Existing Category')
|
||||
->call('create')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('allows same name on update of same category', function () {
|
||||
$permission = Permission::where('name', 'update category')->first();
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$category = CategoryModel::factory()->create(['name' => 'Original Name']);
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Kategori', $category->hash)
|
||||
->set('form.name', 'Original Name')
|
||||
->call('update')
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
|
||||
it('validates unique name on update with different category name', function () {
|
||||
$permission = Permission::where('name', 'update category')->first();
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$category1 = CategoryModel::factory()->create(['name' => 'Category One']);
|
||||
$category2 = CategoryModel::factory()->create(['name' => 'Category Two']);
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->dispatch('modal:open', 'update', 'Edit Kategori', $category1->hash)
|
||||
->set('form.name', 'Category Two')
|
||||
->call('update')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates name uniqueness ignores soft deleted categories', function () {
|
||||
$deletedCategory = CategoryModel::factory()->create(['name' => 'Deleted Category']);
|
||||
$deletedCategory->delete();
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Kategori')
|
||||
->set('form.name', 'Deleted Category')
|
||||
->call('create')
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
|
||||
it('validates description is nullable', function () {
|
||||
mountCategoryComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Kategori')
|
||||
->set('form.name', 'Test Category')
|
||||
->set('form.description', null)
|
||||
->call('create')
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Integration Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('creates category and generates slug automatically', function () {
|
||||
mountCategoryComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Kategori')
|
||||
->set('form.name', 'Test Category Name')
|
||||
->call('create')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$category = CategoryModel::first();
|
||||
expect($category->slug)->not->toBeNull();
|
||||
expect($category->slug)->toBeString();
|
||||
});
|
||||
|
||||
it('maintains data integrity after multiple operations', function () {
|
||||
$createPermission = Permission::where('name', 'create category')->first();
|
||||
$updatePermission = Permission::where('name', 'update category')->first();
|
||||
$this->user->givePermissionTo([$createPermission, $updatePermission]);
|
||||
|
||||
// Create categories
|
||||
mountCategoryComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Kategori')
|
||||
->set('form.name', 'Category One')
|
||||
->call('create');
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Kategori')
|
||||
->set('form.name', 'Category Two')
|
||||
->call('create');
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Kategori')
|
||||
->set('form.name', 'Category Three')
|
||||
->call('create');
|
||||
|
||||
// Update category
|
||||
$category2 = CategoryModel::where('name', 'Category Two')->first();
|
||||
mountCategoryComponent($this->user)
|
||||
->dispatch('modal:open', 'update', 'Edit Kategori', $category2->hash)
|
||||
->set('form.name', 'Category Two Updated')
|
||||
->call('update');
|
||||
|
||||
// Delete category
|
||||
$category1 = CategoryModel::where('name', 'Category One')->first();
|
||||
mountCategoryComponent($this->user)
|
||||
->call('delete', $category1);
|
||||
|
||||
expect(CategoryModel::count())->toBe(2);
|
||||
$remainingCategories = CategoryModel::orderBy('sort_order')->get();
|
||||
expect($remainingCategories[0]->name)->toBe('Category Two Updated');
|
||||
expect($remainingCategories[0]->sort_order)->toBe(1);
|
||||
expect($remainingCategories[1]->name)->toBe('Category Three');
|
||||
expect($remainingCategories[1]->sort_order)->toBe(2);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Notification Subscription Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('subscribes to push notifications successfully', function () {
|
||||
$subscription = [
|
||||
'endpoint' => 'https://fcm.googleapis.com/fcm/send/example',
|
||||
'keys' => [
|
||||
'p256dh' => 'example_p256dh_key',
|
||||
'auth' => 'example_auth_key',
|
||||
],
|
||||
];
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('notificationSubscribe', json_encode($subscription));
|
||||
|
||||
expect(PushNotification::count())->toBe(1);
|
||||
$notification = PushNotification::first();
|
||||
expect($notification->user_id)->toBe($this->user->id);
|
||||
expect($notification->subscriptions)->toBe(json_encode($subscription));
|
||||
});
|
||||
|
||||
it('does not create duplicate push notification subscription', function () {
|
||||
$subscription = [
|
||||
'endpoint' => 'https://fcm.googleapis.com/fcm/send/example',
|
||||
'keys' => [
|
||||
'p256dh' => 'example_p256dh_key',
|
||||
'auth' => 'example_auth_key',
|
||||
],
|
||||
];
|
||||
|
||||
// Subscribe first time
|
||||
mountCategoryComponent($this->user)
|
||||
->call('notificationSubscribe', json_encode($subscription));
|
||||
|
||||
// Try to subscribe again with same auth key
|
||||
mountCategoryComponent($this->user)
|
||||
->call('notificationSubscribe', json_encode($subscription));
|
||||
|
||||
expect(PushNotification::count())->toBe(1);
|
||||
});
|
||||
|
||||
it('creates new push notification subscription for different auth keys', function () {
|
||||
// Create existing subscription
|
||||
PushNotification::create([
|
||||
'user_id' => $this->user->id,
|
||||
'subscriptions' => json_encode([
|
||||
'endpoint' => 'old_endpoint',
|
||||
'keys' => ['p256dh' => 'old_p256dh', 'auth' => 'old_auth'],
|
||||
]),
|
||||
]);
|
||||
|
||||
$newSubscription = [
|
||||
'endpoint' => 'https://fcm.googleapis.com/fcm/send/new_example',
|
||||
'keys' => [
|
||||
'p256dh' => 'new_p256dh_key',
|
||||
'auth' => 'new_auth_key', // Different auth key
|
||||
],
|
||||
];
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('notificationSubscribe', json_encode($newSubscription));
|
||||
|
||||
// Should create a new record since auth keys are different
|
||||
expect(PushNotification::count())->toBe(2);
|
||||
$notifications = PushNotification::where('user_id', $this->user->id)->get();
|
||||
expect($notifications)->toHaveCount(2);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Confirmation Dialog Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('handles confirmation action for delete', function () {
|
||||
$category = CategoryModel::factory()->create();
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('confirmAction', $category->hash, 'delete');
|
||||
|
||||
expect($this->user->fresh()->confirmingId)->toBeNull(); // Property is on component, not user
|
||||
});
|
||||
|
||||
it('sets confirming properties correctly', function () {
|
||||
$component = mountCategoryComponent($this->user);
|
||||
|
||||
$component->call('confirmAction', 'test-id', 'custom_action');
|
||||
|
||||
// Since these are public properties on the component, we can access them
|
||||
// But Livewire components don't expose these properties directly in tests
|
||||
// Let's test the delete method which should work with confirmation
|
||||
$category = CategoryModel::factory()->create();
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('delete', $category)
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Toast Notification Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('shows success toast on category creation', function () {
|
||||
mountCategoryComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Kategori')
|
||||
->set('form.name', 'Test Category')
|
||||
->call('create')
|
||||
->assertHasNoErrors();
|
||||
|
||||
// Toast assertions are implicit in the component's toast() calls
|
||||
// We can verify the category was created successfully
|
||||
expect(CategoryModel::where('name', 'Test Category')->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
it('shows success toast on category update', function () {
|
||||
$permission = Permission::where('name', 'update category')->first();
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$category = CategoryModel::factory()->create(['name' => 'Old Name']);
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Kategori', $category->hash)
|
||||
->set('form.name', 'Updated Name')
|
||||
->call('update')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$category->refresh();
|
||||
expect($category->name)->toBe('Updated Name');
|
||||
});
|
||||
|
||||
it('shows success toast on category deletion', function () {
|
||||
$category = CategoryModel::factory()->create(['name' => 'Category to Delete']);
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('delete', $category)
|
||||
->assertHasNoErrors();
|
||||
|
||||
$this->assertSoftDeleted('categories', ['id' => $category->id]);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Real-time Validation Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates name field in real-time on update', function () {
|
||||
$component = mountCategoryComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Kategori');
|
||||
|
||||
// Set empty name - should trigger validation
|
||||
$component->set('form.name', '')
|
||||
->assertHasErrors(['form.name']);
|
||||
|
||||
// Set valid name - should clear errors
|
||||
$component->set('form.name', 'Valid Category Name')
|
||||
->assertHasNoErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates name length in real-time', function () {
|
||||
$component = mountCategoryComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Kategori');
|
||||
|
||||
// Set name longer than 20 characters
|
||||
$component->set('form.name', str_repeat('a', 21))
|
||||
->assertHasErrors(['form.name']);
|
||||
|
||||
// Set valid length name
|
||||
$component->set('form.name', str_repeat('a', 20))
|
||||
->assertHasNoErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates name uniqueness in real-time', function () {
|
||||
$existingCategory = CategoryModel::factory()->create(['name' => 'Existing Name']);
|
||||
|
||||
$component = mountCategoryComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Kategori');
|
||||
|
||||
// Try to set duplicate name
|
||||
$component->set('form.name', 'Existing Name')
|
||||
->assertHasErrors(['form.name']);
|
||||
|
||||
// Set unique name
|
||||
$component->set('form.name', 'Unique Name')
|
||||
->assertHasNoErrors(['form.name']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authorization Edge Cases Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('handles unauthorized user with revoked permissions', function () {
|
||||
// Revoke all category permissions
|
||||
$this->ownerRole->revokePermissionTo(['create category', 'update category', 'delete category']);
|
||||
|
||||
// Define gates to return false
|
||||
Gate::define('create category', fn () => false);
|
||||
Gate::define('update category', fn () => false);
|
||||
Gate::define('delete category', fn () => false);
|
||||
|
||||
$category = CategoryModel::factory()->create();
|
||||
|
||||
// Test create
|
||||
mountCategoryComponent($this->user)
|
||||
->call('create')
|
||||
->assertForbidden();
|
||||
|
||||
// Test update
|
||||
mountCategoryComponent($this->user)
|
||||
->call('update')
|
||||
->assertForbidden();
|
||||
|
||||
// Test delete
|
||||
mountCategoryComponent($this->user)
|
||||
->call('delete', $category)
|
||||
->assertForbidden();
|
||||
|
||||
// Test sort order
|
||||
mountCategoryComponent($this->user)
|
||||
->call('sortOrder', $category, 'up')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('handles user with partial permissions correctly', function () {
|
||||
// Remove owner role and create a user with limited permissions
|
||||
$this->user->roles()->detach();
|
||||
$limitedRole = Role::create(['name' => 'Limited User']);
|
||||
$this->user->roles()->attach($limitedRole->id);
|
||||
|
||||
// Give only create permission
|
||||
$createPermission = Permission::where('name', 'create category')->first();
|
||||
$limitedRole->givePermissionTo($createPermission);
|
||||
|
||||
$category = CategoryModel::factory()->create();
|
||||
|
||||
// Should be able to create
|
||||
mountCategoryComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Kategori')
|
||||
->set('form.name', 'New Category')
|
||||
->call('create')
|
||||
->assertHasNoErrors();
|
||||
|
||||
// Should not be able to update
|
||||
mountCategoryComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Kategori', $category->hash)
|
||||
->set('form.name', 'Updated Name')
|
||||
->call('update')
|
||||
->assertForbidden();
|
||||
|
||||
// Should not be able to delete
|
||||
mountCategoryComponent($this->user)
|
||||
->call('delete', $category)
|
||||
->assertForbidden();
|
||||
|
||||
// Should not be able to sort
|
||||
mountCategoryComponent($this->user)
|
||||
->call('sortOrder', $category, 'up')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Error Handling and Edge Cases Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('handles invalid category hash by throwing exception', function () {
|
||||
// The byHashOrFail method throws ModelNotFoundException for invalid hashes
|
||||
// This is expected behavior - invalid hashes should result in errors
|
||||
expect(fn () => mountCategoryComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Kategori', 'invalid-hash'))
|
||||
->toThrow(\Illuminate\Database\Eloquent\ModelNotFoundException::class);
|
||||
});
|
||||
|
||||
it('handles sort order operations on empty database', function () {
|
||||
// Ensure no categories exist
|
||||
CategoryModel::query()->delete();
|
||||
|
||||
$category = CategoryModel::factory()->create();
|
||||
|
||||
// Test moving up when it's the only/first category
|
||||
mountCategoryComponent($this->user)
|
||||
->call('sortOrder', $category, 'up')
|
||||
->assertHasNoErrors();
|
||||
|
||||
// Test moving down when it's the only/last category
|
||||
mountCategoryComponent($this->user)
|
||||
->call('sortOrder', $category, 'down')
|
||||
->assertHasNoErrors();
|
||||
|
||||
// Test moving to first when it's already first
|
||||
mountCategoryComponent($this->user)
|
||||
->call('sortOrder', $category, 'first')
|
||||
->assertHasNoErrors();
|
||||
|
||||
// Test moving to last when it's already last
|
||||
mountCategoryComponent($this->user)
|
||||
->call('sortOrder', $category, 'last')
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
|
||||
it('handles concurrent category creation correctly', function () {
|
||||
// Create multiple categories in sequence to test sort_order as first position
|
||||
for ($i = 1; $i <= 5; $i++) {
|
||||
mountCategoryComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Kategori')
|
||||
->set('form.name', "Category {$i}")
|
||||
->call('create')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$category = CategoryModel::where('name', "Category {$i}")->first();
|
||||
expect($category->sort_order)->toBe(1); // New categories always get sort_order = 1 (first position)
|
||||
}
|
||||
|
||||
expect(CategoryModel::count())->toBe(5);
|
||||
});
|
||||
|
||||
it('handles category deletion with cascade relationships', function () {
|
||||
// Note: Category model has cascadeDeletes = ['perfumes']
|
||||
// This test ensures the deletion works even with related models
|
||||
$category = CategoryModel::factory()->create();
|
||||
|
||||
mountCategoryComponent($this->user)
|
||||
->call('delete', $category)
|
||||
->assertHasNoErrors();
|
||||
|
||||
$this->assertSoftDeleted('categories', ['id' => $category->id]);
|
||||
});
|
||||
|
||||
it('validates form data integrity during operations', function () {
|
||||
// Test that form data is properly reset between operations
|
||||
$category1 = CategoryModel::factory()->create(['name' => 'First Category']);
|
||||
$category2 = CategoryModel::factory()->create(['name' => 'Second Category']);
|
||||
|
||||
$component = mountCategoryComponent($this->user);
|
||||
|
||||
// Open modal for first category
|
||||
$component->call('openModal', 'update', 'Edit Kategori', $category1->hash);
|
||||
expect($component->form->name)->toBe('First Category');
|
||||
|
||||
// Open modal for second category without saving
|
||||
$component->call('openModal', 'update', 'Edit Kategori', $category2->hash);
|
||||
expect($component->form->name)->toBe('Second Category');
|
||||
|
||||
// Verify first category wasn't modified
|
||||
$category1->refresh();
|
||||
expect($category1->name)->toBe('First Category');
|
||||
});
|
||||
@ -1,475 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\AuditStockStatus;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\Perfume;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
|
||||
$this->ownerRole = Role::where('name', 'Owner')->first();
|
||||
$this->user->roles()->attach($this->ownerRole->id);
|
||||
|
||||
// Create test outlets
|
||||
$this->outlets = Outlet::factory()->count(3)->create();
|
||||
});
|
||||
|
||||
function mountPerfumeAuditComponent(User $user, Outlet $outlet, Perfume $perfume)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(\App\Livewire\Studio\Catalog\Perfume\Audit::class, ['outlet' => $outlet, 'perfume' => $perfume]);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Audit Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders perfume audit page successfully', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
mountPerfumeAuditComponent($this->user, $outlet, $perfume)
|
||||
->assertViewIs('components.sections.ui.audit')
|
||||
->assertViewHas('pageTitle', "Audit {$perfume->name} di {$outlet->name}")
|
||||
->assertViewHas('module', 'perfume');
|
||||
});
|
||||
|
||||
it('sets perfume label correctly on mount', function () {
|
||||
$perfume = Perfume::factory()->create(['name' => 'Test Perfume']);
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
$component = mountPerfumeAuditComponent($this->user, $outlet, $perfume);
|
||||
|
||||
expect($component->label)->toBe('Test Perfume di '.$outlet->name);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Activity Logs Display Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('displays perfume activity logs correctly', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
// Create some activity logs with expected properties
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock updated',
|
||||
'subject_type' => get_class($perfume),
|
||||
'subject_id' => $perfume->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 5,
|
||||
'quantity_change' => 10,
|
||||
'new_stock' => 15,
|
||||
],
|
||||
'event' => AuditStockStatus::INCREASED->value,
|
||||
]);
|
||||
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock decreased',
|
||||
'subject_type' => get_class($perfume),
|
||||
'subject_id' => $perfume->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 15,
|
||||
'quantity_change' => -5,
|
||||
'new_stock' => 10,
|
||||
],
|
||||
'event' => AuditStockStatus::DECREASED->value,
|
||||
]);
|
||||
|
||||
$component = mountPerfumeAuditComponent($this->user, $outlet, $perfume);
|
||||
|
||||
expect($component->activityLogs)->toHaveCount(2);
|
||||
expect($component->activityLogs->first()->event['label'])->toBe(AuditStockStatus::INCREASED->label());
|
||||
expect($component->activityLogs->first()->event['color'])->toBe(AuditStockStatus::INCREASED->color());
|
||||
expect($component->activityLogs->last()->event['label'])->toBe(AuditStockStatus::DECREASED->label());
|
||||
expect($component->activityLogs->last()->event['color'])->toBe(AuditStockStatus::DECREASED->color());
|
||||
});
|
||||
|
||||
it('filters perfume activity logs by outlet', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
$outlet1 = $this->outlets->first();
|
||||
$outlet2 = $this->outlets->last();
|
||||
|
||||
// Create activity for outlet1
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock updated outlet1',
|
||||
'subject_type' => get_class($perfume),
|
||||
'subject_id' => $perfume->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet1->id,
|
||||
'previous_stock' => 5,
|
||||
'quantity_change' => 10,
|
||||
'new_stock' => 15,
|
||||
],
|
||||
'event' => AuditStockStatus::UPDATED->value,
|
||||
]);
|
||||
|
||||
// Create activity for outlet2
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock increased outlet2',
|
||||
'subject_type' => get_class($perfume),
|
||||
'subject_id' => $perfume->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet2->id,
|
||||
'previous_stock' => 0,
|
||||
'quantity_change' => 20,
|
||||
'new_stock' => 20,
|
||||
],
|
||||
'event' => AuditStockStatus::INCREASED->value,
|
||||
]);
|
||||
|
||||
$component1 = mountPerfumeAuditComponent($this->user, $outlet1, $perfume);
|
||||
$component2 = mountPerfumeAuditComponent($this->user, $outlet2, $perfume);
|
||||
|
||||
expect($component1->activityLogs)->toHaveCount(1);
|
||||
expect($component1->activityLogs[0]['description'])->toBe('Stock updated outlet1');
|
||||
|
||||
expect($component2->activityLogs)->toHaveCount(1);
|
||||
expect($component2->activityLogs[0]['description'])->toBe('Stock increased outlet2');
|
||||
});
|
||||
|
||||
it('orders perfume activity logs by latest first', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
// Create activities with different timestamps
|
||||
$activity1 = Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'first activity',
|
||||
'subject_type' => get_class($perfume),
|
||||
'subject_id' => $perfume->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 5,
|
||||
'quantity_change' => 5,
|
||||
'new_stock' => 10,
|
||||
],
|
||||
'event' => AuditStockStatus::UPDATED->value,
|
||||
'created_at' => now()->subMinutes(5),
|
||||
]);
|
||||
|
||||
$activity2 = Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'second activity',
|
||||
'subject_type' => get_class($perfume),
|
||||
'subject_id' => $perfume->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 10,
|
||||
'quantity_change' => 10,
|
||||
'new_stock' => 20,
|
||||
],
|
||||
'event' => AuditStockStatus::INCREASED->value,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
$component = mountPerfumeAuditComponent($this->user, $outlet, $perfume);
|
||||
|
||||
expect($component->activityLogs)->toHaveCount(2);
|
||||
expect($component->activityLogs[0]['description'])->toBe('second activity');
|
||||
expect($component->activityLogs[1]['description'])->toBe('first activity');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Activity Logs Content Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('maps perfume activity events to correct labels and colors', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
// Test INCREASED event
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock increased',
|
||||
'subject_type' => get_class($perfume),
|
||||
'subject_id' => $perfume->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 10,
|
||||
'quantity_change' => 5,
|
||||
'new_stock' => 15,
|
||||
],
|
||||
'event' => AuditStockStatus::INCREASED->value,
|
||||
]);
|
||||
|
||||
// Test DECREASED event
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock decreased',
|
||||
'subject_type' => get_class($perfume),
|
||||
'subject_id' => $perfume->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 15,
|
||||
'quantity_change' => -3,
|
||||
'new_stock' => 12,
|
||||
],
|
||||
'event' => AuditStockStatus::DECREASED->value,
|
||||
]);
|
||||
|
||||
// Test UPDATED event
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock updated',
|
||||
'subject_type' => get_class($perfume),
|
||||
'subject_id' => $perfume->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 12,
|
||||
'quantity_change' => 0,
|
||||
'new_stock' => 12,
|
||||
],
|
||||
'event' => AuditStockStatus::UPDATED->value,
|
||||
]);
|
||||
|
||||
$component = mountPerfumeAuditComponent($this->user, $outlet, $perfume);
|
||||
|
||||
expect($component->activityLogs)->toHaveCount(3);
|
||||
|
||||
// Check INCREASED event mapping
|
||||
$increasedLog = collect($component->activityLogs)->firstWhere('event.label', AuditStockStatus::INCREASED->label());
|
||||
expect($increasedLog['event']['color'])->toBe(AuditStockStatus::INCREASED->color());
|
||||
|
||||
// Check DECREASED event mapping
|
||||
$decreasedLog = collect($component->activityLogs)->firstWhere('event.label', AuditStockStatus::DECREASED->label());
|
||||
expect($decreasedLog['event']['color'])->toBe(AuditStockStatus::DECREASED->color());
|
||||
|
||||
// Check UPDATED event mapping
|
||||
$updatedLog = collect($component->activityLogs)->firstWhere('event.label', AuditStockStatus::UPDATED->label());
|
||||
expect($updatedLog['event']['color'])->toBe(AuditStockStatus::UPDATED->color());
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Empty State Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('handles empty perfume activity logs in audit component', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
$component = mountPerfumeAuditComponent($this->user, $outlet, $perfume);
|
||||
|
||||
expect($component->activityLogs)->toBeInstanceOf(\Illuminate\Database\Eloquent\Collection::class);
|
||||
expect($component->activityLogs)->toHaveCount(0);
|
||||
});
|
||||
|
||||
it('handles perfume with no activities in specific outlet', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
$outlet1 = $this->outlets->first();
|
||||
$outlet2 = $this->outlets->last();
|
||||
|
||||
// Create activity only for outlet2
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock updated',
|
||||
'subject_type' => get_class($perfume),
|
||||
'subject_id' => $perfume->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet2->id,
|
||||
'previous_stock' => 5,
|
||||
'quantity_change' => 5,
|
||||
'new_stock' => 10,
|
||||
],
|
||||
'event' => AuditStockStatus::UPDATED->value,
|
||||
]);
|
||||
|
||||
$component1 = mountPerfumeAuditComponent($this->user, $outlet1, $perfume);
|
||||
$component2 = mountPerfumeAuditComponent($this->user, $outlet2, $perfume);
|
||||
|
||||
expect($component1->activityLogs)->toHaveCount(0);
|
||||
expect($component2->activityLogs)->toHaveCount(1);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Integration Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('displays real perfume activity log data correctly', function () {
|
||||
$perfume = Perfume::factory()->create(['name' => 'Integration Perfume']);
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
$activity = Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock adjustment for integration test',
|
||||
'subject_type' => get_class($perfume),
|
||||
'subject_id' => $perfume->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 100,
|
||||
'quantity_change' => 25,
|
||||
'new_stock' => 125,
|
||||
],
|
||||
'event' => AuditStockStatus::INCREASED->value,
|
||||
]);
|
||||
|
||||
$component = mountPerfumeAuditComponent($this->user, $outlet, $perfume);
|
||||
|
||||
expect($component->activityLogs)->toHaveCount(1);
|
||||
|
||||
$log = $component->activityLogs->first();
|
||||
expect($log->description)->toBe('Stock adjustment for integration test');
|
||||
expect($log->properties['previous_stock'])->toBe(100);
|
||||
expect($log->properties['quantity_change'])->toBe(25);
|
||||
expect($log->properties['new_stock'])->toBe(125);
|
||||
expect($log->event['label'])->toBe(AuditStockStatus::INCREASED->label());
|
||||
expect($log->event['color'])->toBe(AuditStockStatus::INCREASED->color());
|
||||
});
|
||||
|
||||
it('handles multiple perfume activities across different outlets', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
$outlet1 = $this->outlets->first();
|
||||
$outlet2 = $this->outlets->last();
|
||||
|
||||
// Create activities for both outlets
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Activity in outlet 1',
|
||||
'subject_type' => get_class($perfume),
|
||||
'subject_id' => $perfume->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet1->id,
|
||||
'previous_stock' => 10,
|
||||
'quantity_change' => 5,
|
||||
'new_stock' => 15,
|
||||
],
|
||||
'event' => AuditStockStatus::INCREASED->value,
|
||||
]);
|
||||
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Activity in outlet 2',
|
||||
'subject_type' => get_class($perfume),
|
||||
'subject_id' => $perfume->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet2->id,
|
||||
'previous_stock' => 20,
|
||||
'quantity_change' => -3,
|
||||
'new_stock' => 17,
|
||||
],
|
||||
'event' => AuditStockStatus::DECREASED->value,
|
||||
]);
|
||||
|
||||
$component1 = mountPerfumeAuditComponent($this->user, $outlet1, $perfume);
|
||||
$component2 = mountPerfumeAuditComponent($this->user, $outlet2, $perfume);
|
||||
|
||||
expect($component1->activityLogs)->toHaveCount(1);
|
||||
expect($component1->activityLogs[0]['description'])->toBe('Activity in outlet 1');
|
||||
|
||||
expect($component2->activityLogs)->toHaveCount(1);
|
||||
expect($component2->activityLogs[0]['description'])->toBe('Activity in outlet 2');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Edge Cases and Error Handling
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('handles perfume activity logs with missing properties gracefully', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
// Create activity with incomplete properties (shouldn't happen in real usage)
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Incomplete activity',
|
||||
'subject_type' => get_class($perfume),
|
||||
'subject_id' => $perfume->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 0, // Add required properties
|
||||
'quantity_change' => 0,
|
||||
'new_stock' => 0,
|
||||
],
|
||||
'event' => AuditStockStatus::UPDATED->value,
|
||||
]);
|
||||
|
||||
$component = mountPerfumeAuditComponent($this->user, $outlet, $perfume);
|
||||
|
||||
expect($component->activityLogs)->toHaveCount(1);
|
||||
// Component should render correctly with all required properties
|
||||
});
|
||||
|
||||
it('handles perfume activities with null causer', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Activity with no causer',
|
||||
'subject_type' => get_class($perfume),
|
||||
'subject_id' => $perfume->id,
|
||||
'causer_type' => null,
|
||||
'causer_id' => null,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 5,
|
||||
'quantity_change' => 5,
|
||||
'new_stock' => 10,
|
||||
],
|
||||
'event' => AuditStockStatus::INCREASED->value,
|
||||
]);
|
||||
|
||||
$component = mountPerfumeAuditComponent($this->user, $outlet, $perfume);
|
||||
|
||||
expect($component->activityLogs)->toHaveCount(1);
|
||||
expect($component->activityLogs[0]['user'])->toBeNull();
|
||||
});
|
||||
@ -1,455 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\Concentration;
|
||||
use App\Models\Brand;
|
||||
use App\Models\Category;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\Perfume;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
|
||||
$this->ownerRole = Role::where('name', 'Owner')->first();
|
||||
$this->user->roles()->attach($this->ownerRole->id);
|
||||
|
||||
// Create test data
|
||||
$this->outlets = Outlet::factory()->count(3)->create();
|
||||
$this->brands = Brand::factory()->count(2)->create();
|
||||
$this->categories = Category::factory()->count(3)->create();
|
||||
});
|
||||
|
||||
function mountPerfumeCreateComponent(User $user)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(\App\Livewire\Studio\Catalog\Perfume\Create::class);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Create Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders perfume create page successfully', function () {
|
||||
mountPerfumeCreateComponent($this->user)
|
||||
->assertViewIs('livewire.studio.catalog.perfume.form')
|
||||
->assertViewHas('pageTitle', 'Tambah Parfum');
|
||||
});
|
||||
|
||||
it('initializes perfume form properties on mount', function () {
|
||||
$component = mountPerfumeCreateComponent($this->user);
|
||||
|
||||
expect($component->form->name)->toBe('');
|
||||
expect($component->form->sku)->toBe('');
|
||||
expect($component->form->brand_id)->toBeNull();
|
||||
expect($component->form->cost_price)->toBe('');
|
||||
expect($component->form->sale_price)->toBe('');
|
||||
expect($component->form->concentration)->toBe('2');
|
||||
expect($component->form->base_notes)->toBeNull();
|
||||
expect($component->form->middle_notes)->toBeNull();
|
||||
expect($component->form->top_notes)->toBeNull();
|
||||
expect($component->form->description)->toBeNull();
|
||||
expect($component->form->category_ids)->toBe([]);
|
||||
expect($component->form->outlet_ids)->toBe([]);
|
||||
expect($component->form->image)->toBe([]);
|
||||
expect($component->outlets)->toHaveCount(3);
|
||||
expect($component->brands)->toHaveCount(2);
|
||||
expect($component->categories)->toHaveCount(3);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Create Perfume Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('creates perfume successfully when authorized', function () {
|
||||
$data = [
|
||||
'name' => 'New Perfume',
|
||||
'sku' => 'NP001',
|
||||
'brand_id' => $this->brands->first()->id,
|
||||
'cost_price' => '150000',
|
||||
'sale_price' => '250000',
|
||||
'concentration' => Concentration::EAU_DE_PERFUME->value,
|
||||
'base_notes' => 'Vanilla, Musk',
|
||||
'middle_notes' => 'Jasmine, Rose',
|
||||
'top_notes' => 'Bergamot, Lemon',
|
||||
'description' => 'A beautiful fragrance',
|
||||
'category_ids' => [$this->categories->first()->id, $this->categories->last()->id],
|
||||
'outlet_ids' => [$this->outlets->first()->id, $this->outlets->last()->id],
|
||||
];
|
||||
|
||||
mountPerfumeCreateComponent($this->user)
|
||||
->set('form.name', $data['name'])
|
||||
->set('form.sku', $data['sku'])
|
||||
->set('form.brand_id', $data['brand_id'])
|
||||
->set('form.cost_price', $data['cost_price'])
|
||||
->set('form.sale_price', $data['sale_price'])
|
||||
->set('form.concentration', $data['concentration'])
|
||||
->set('form.base_notes', $data['base_notes'])
|
||||
->set('form.middle_notes', $data['middle_notes'])
|
||||
->set('form.top_notes', $data['top_notes'])
|
||||
->set('form.description', $data['description'])
|
||||
->set('form.category_ids', $data['category_ids'])
|
||||
->set('form.outlet_ids', $data['outlet_ids'])
|
||||
->call('save')
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect(route('studio.catalog.perfume.index'));
|
||||
|
||||
expect(Perfume::count())->toBe(1);
|
||||
$perfume = Perfume::first();
|
||||
expect($perfume->name)->toBe($data['name']);
|
||||
expect($perfume->sku)->toBe($data['sku']);
|
||||
expect($perfume->brand_id)->toBe($data['brand_id']);
|
||||
expect($perfume->cost_price)->toBe(150000);
|
||||
expect($perfume->sale_price)->toBe(250000);
|
||||
expect($perfume->concentration->value)->toBe($data['concentration']);
|
||||
expect($perfume->base_notes)->toBe($data['base_notes']);
|
||||
expect($perfume->middle_notes)->toBe($data['middle_notes']);
|
||||
expect($perfume->top_notes)->toBe($data['top_notes']);
|
||||
expect($perfume->description)->toBe($data['description']);
|
||||
expect($perfume->categories)->toHaveCount(2);
|
||||
expect($perfume->outlets)->toHaveCount(2);
|
||||
});
|
||||
|
||||
it('prevents perfume creation when unauthorized', function () {
|
||||
$this->ownerRole->revokePermissionTo('create perfume');
|
||||
Gate::define('create perfume', fn () => false);
|
||||
|
||||
mountPerfumeCreateComponent($this->user)
|
||||
->set('form.name', 'Test Perfume')
|
||||
->set('form.sku', 'TP001')
|
||||
->set('form.sale_price', '100000')
|
||||
->set('form.category_ids', [$this->categories->first()->id])
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('creates perfume with empty cost_price for non-owner roles', function () {
|
||||
// Create user with limited role
|
||||
$this->user->roles()->detach();
|
||||
$limitedRole = Role::create(['name' => 'Cashier']);
|
||||
$this->user->roles()->attach($limitedRole->id);
|
||||
|
||||
$createPermission = Permission::where('name', 'create perfume')->first();
|
||||
$limitedRole->givePermissionTo($createPermission);
|
||||
|
||||
mountPerfumeCreateComponent($this->user)
|
||||
->set('form.name', 'Test Perfume')
|
||||
->set('form.sku', 'TP001')
|
||||
->set('form.cost_price', '') // Empty cost price
|
||||
->set('form.sale_price', '200000')
|
||||
->set('form.category_ids', [$this->categories->first()->id])
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$perfume = Perfume::first();
|
||||
expect($perfume->cost_price)->toBe(0);
|
||||
});
|
||||
|
||||
it('creates perfume with optional brand', function () {
|
||||
mountPerfumeCreateComponent($this->user)
|
||||
->set('form.name', 'Perfume Without Brand')
|
||||
->set('form.sku', 'PWB001')
|
||||
->set('form.brand_id', null) // No brand
|
||||
->set('form.cost_price', '100000') // Required for Owner role
|
||||
->set('form.sale_price', '150000')
|
||||
->set('form.category_ids', [$this->categories->first()->id])
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$perfume = Perfume::first();
|
||||
expect($perfume->brand_id)->toBeNull();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Form Validation Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates required name field on perfume create', function () {
|
||||
mountPerfumeCreateComponent($this->user)
|
||||
->set('form.name', '')
|
||||
->set('form.sku', 'TP001')
|
||||
->set('form.sale_price', '100000')
|
||||
->set('form.category_ids', [$this->categories->first()->id])
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates name field max length on perfume create', function () {
|
||||
mountPerfumeCreateComponent($this->user)
|
||||
->set('form.name', str_repeat('a', 51))
|
||||
->set('form.sku', 'TP001')
|
||||
->set('form.sale_price', '100000')
|
||||
->set('form.category_ids', [$this->categories->first()->id])
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates required sku field on perfume create', function () {
|
||||
mountPerfumeCreateComponent($this->user)
|
||||
->set('form.name', 'Test Perfume')
|
||||
->set('form.sku', '')
|
||||
->set('form.sale_price', '100000')
|
||||
->set('form.category_ids', [$this->categories->first()->id])
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.sku']);
|
||||
});
|
||||
|
||||
it('validates sku field max length on perfume create', function () {
|
||||
mountPerfumeCreateComponent($this->user)
|
||||
->set('form.name', 'Test Perfume')
|
||||
->set('form.sku', str_repeat('a', 21))
|
||||
->set('form.sale_price', '100000')
|
||||
->set('form.category_ids', [$this->categories->first()->id])
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.sku']);
|
||||
});
|
||||
|
||||
it('validates sku uniqueness on perfume create', function () {
|
||||
Perfume::factory()->create(['sku' => 'EXISTING001']);
|
||||
|
||||
mountPerfumeCreateComponent($this->user)
|
||||
->set('form.name', 'Test Perfume')
|
||||
->set('form.sku', 'EXISTING001')
|
||||
->set('form.sale_price', '100000')
|
||||
->set('form.category_ids', [$this->categories->first()->id])
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.sku']);
|
||||
});
|
||||
|
||||
it('validates brand_id exists if provided', function () {
|
||||
mountPerfumeCreateComponent($this->user)
|
||||
->set('form.name', 'Test Perfume')
|
||||
->set('form.sku', 'TP001')
|
||||
->set('form.brand_id', 999) // Non-existent brand
|
||||
->set('form.sale_price', '100000')
|
||||
->set('form.category_ids', [$this->categories->first()->id])
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.brand_id']);
|
||||
});
|
||||
|
||||
it('validates required sale_price field on perfume create', function () {
|
||||
mountPerfumeCreateComponent($this->user)
|
||||
->set('form.name', 'Test Perfume')
|
||||
->set('form.sku', 'TP001')
|
||||
->set('form.sale_price', '')
|
||||
->set('form.category_ids', [$this->categories->first()->id])
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.sale_price']);
|
||||
});
|
||||
|
||||
it('validates concentration is valid enum value', function () {
|
||||
mountPerfumeCreateComponent($this->user)
|
||||
->set('form.name', 'Test Perfume')
|
||||
->set('form.sku', 'TP001')
|
||||
->set('form.sale_price', '100000')
|
||||
->set('form.concentration', 'invalid')
|
||||
->set('form.category_ids', [$this->categories->first()->id])
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.concentration']);
|
||||
});
|
||||
|
||||
it('validates required category_ids field on perfume create', function () {
|
||||
mountPerfumeCreateComponent($this->user)
|
||||
->set('form.name', 'Test Perfume')
|
||||
->set('form.sku', 'TP001')
|
||||
->set('form.sale_price', '100000')
|
||||
->set('form.category_ids', [])
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.category_ids']);
|
||||
});
|
||||
|
||||
it('validates category_ids contains valid category ids', function () {
|
||||
mountPerfumeCreateComponent($this->user)
|
||||
->set('form.name', 'Test Perfume')
|
||||
->set('form.sku', 'TP001')
|
||||
->set('form.sale_price', '100000')
|
||||
->set('form.category_ids', [999])
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.category_ids.*']);
|
||||
});
|
||||
|
||||
it('validates required outlet_ids field on perfume create', function () {
|
||||
mountPerfumeCreateComponent($this->user)
|
||||
->set('form.name', 'Test Perfume')
|
||||
->set('form.sku', 'TP001')
|
||||
->set('form.sale_price', '100000')
|
||||
->set('form.category_ids', [$this->categories->first()->id])
|
||||
->set('form.outlet_ids', [])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.outlet_ids']);
|
||||
});
|
||||
|
||||
it('validates outlet_ids contains valid outlet ids on perfume create', function () {
|
||||
mountPerfumeCreateComponent($this->user)
|
||||
->set('form.name', 'Test Perfume')
|
||||
->set('form.sku', 'TP001')
|
||||
->set('form.sale_price', '100000')
|
||||
->set('form.category_ids', [$this->categories->first()->id])
|
||||
->set('form.outlet_ids', [999])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.outlet_ids.*']);
|
||||
});
|
||||
|
||||
it('validates notes fields max length', function () {
|
||||
mountPerfumeCreateComponent($this->user)
|
||||
->set('form.name', 'Test Perfume')
|
||||
->set('form.sku', 'TP001')
|
||||
->set('form.sale_price', '100000')
|
||||
->set('form.base_notes', str_repeat('a', 256)) // Exceeds 255
|
||||
->set('form.category_ids', [$this->categories->first()->id])
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.base_notes']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Real-time Validation Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates perfume name field in real-time', function () {
|
||||
$component = mountPerfumeCreateComponent($this->user);
|
||||
|
||||
// Valid name
|
||||
$component->set('form.name', 'Valid Perfume Name')
|
||||
->assertHasNoErrors(['form.name']);
|
||||
|
||||
// Empty name
|
||||
$component->set('form.name', '')
|
||||
->assertHasErrors(['form.name']);
|
||||
|
||||
// Name too long
|
||||
$component->set('form.name', str_repeat('a', 51))
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates perfume sku field in real-time', function () {
|
||||
$component = mountPerfumeCreateComponent($this->user);
|
||||
|
||||
// Valid SKU
|
||||
$component->set('form.sku', 'VALID001')
|
||||
->assertHasNoErrors(['form.sku']);
|
||||
|
||||
// Empty SKU
|
||||
$component->set('form.sku', '')
|
||||
->assertHasErrors(['form.sku']);
|
||||
|
||||
// SKU too long
|
||||
$component->set('form.sku', str_repeat('a', 21))
|
||||
->assertHasErrors(['form.sku']);
|
||||
|
||||
// Duplicate SKU
|
||||
Perfume::factory()->create(['sku' => 'EXISTING001']);
|
||||
$component->set('form.sku', 'EXISTING001')
|
||||
->assertHasErrors(['form.sku']);
|
||||
});
|
||||
|
||||
it('validates perfume price fields in real-time', function () {
|
||||
$component = mountPerfumeCreateComponent($this->user);
|
||||
|
||||
// Valid sale price
|
||||
$component->set('form.sale_price', '200000')
|
||||
->assertHasNoErrors(['form.sale_price']);
|
||||
|
||||
// Empty sale price
|
||||
$component->set('form.sale_price', '')
|
||||
->assertHasErrors(['form.sale_price']);
|
||||
|
||||
// Negative sale price
|
||||
$component->set('form.sale_price', '-200000')
|
||||
->assertHasErrors(['form.sale_price']);
|
||||
});
|
||||
|
||||
it('validates perfume concentration field in real-time', function () {
|
||||
$component = mountPerfumeCreateComponent($this->user);
|
||||
|
||||
// Valid concentration
|
||||
$component->set('form.concentration', Concentration::EAU_DE_PERFUME->value)
|
||||
->assertHasNoErrors(['form.concentration']);
|
||||
|
||||
// Invalid concentration
|
||||
$component->set('form.concentration', 'invalid')
|
||||
->assertHasErrors(['form.concentration']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Integration Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('creates perfume and attaches to categories and outlets correctly', function () {
|
||||
$category1 = $this->categories->first();
|
||||
$category2 = $this->categories->last();
|
||||
$outlet1 = $this->outlets->first();
|
||||
$outlet2 = $this->outlets->last();
|
||||
$brand = $this->brands->first();
|
||||
|
||||
mountPerfumeCreateComponent($this->user)
|
||||
->set('form.name', 'Integration Test Perfume')
|
||||
->set('form.sku', 'ITP001')
|
||||
->set('form.brand_id', $brand->id)
|
||||
->set('form.cost_price', '200000') // Required for Owner role
|
||||
->set('form.sale_price', '350000')
|
||||
->set('form.concentration', Concentration::EAU_DE_PERFUME->value)
|
||||
->set('form.category_ids', [$category1->id, $category2->id])
|
||||
->set('form.outlet_ids', [$outlet1->id, $outlet2->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$perfume = Perfume::where('sku', 'ITP001')->first();
|
||||
expect($perfume)->not->toBeNull();
|
||||
expect($perfume->brand->id)->toBe($brand->id);
|
||||
expect($perfume->categories)->toHaveCount(2);
|
||||
expect($perfume->outlets)->toHaveCount(2);
|
||||
expect($perfume->categories->pluck('id')->toArray())->toContain($category1->id, $category2->id);
|
||||
expect($perfume->outlets->pluck('id')->toArray())->toContain($outlet1->id, $outlet2->id);
|
||||
});
|
||||
|
||||
it('handles large numbers in perfume price fields', function () {
|
||||
mountPerfumeCreateComponent($this->user)
|
||||
->set('form.name', 'Expensive Perfume')
|
||||
->set('form.sku', 'EP001')
|
||||
->set('form.cost_price', '10000000') // 10 million
|
||||
->set('form.sale_price', '15000000') // 15 million
|
||||
->set('form.category_ids', [$this->categories->first()->id])
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$perfume = Perfume::where('sku', 'EP001')->first();
|
||||
expect($perfume->cost_price)->toBe(10000000);
|
||||
expect($perfume->sale_price)->toBe(15000000);
|
||||
});
|
||||
@ -1,480 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\Concentration;
|
||||
use App\Models\Brand;
|
||||
use App\Models\Category;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\Perfume;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
|
||||
$this->ownerRole = Role::where('name', 'Owner')->first();
|
||||
$this->user->roles()->attach($this->ownerRole->id);
|
||||
|
||||
// Create test data
|
||||
$this->outlets = Outlet::factory()->count(3)->create();
|
||||
$this->brands = Brand::factory()->count(2)->create();
|
||||
$this->categories = Category::factory()->count(3)->create();
|
||||
});
|
||||
|
||||
function mountPerfumeEditComponent(User $user, Perfume $perfume)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(\App\Livewire\Studio\Catalog\Perfume\Edit::class, ['perfume' => $perfume->hash]);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Edit Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders perfume edit page successfully', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
|
||||
mountPerfumeEditComponent($this->user, $perfume)
|
||||
->assertViewIs('livewire.studio.catalog.perfume.form')
|
||||
->assertViewHas('pageTitle', 'Ubah Parfum');
|
||||
});
|
||||
|
||||
it('populates perfume form with perfume data on mount', function () {
|
||||
$brand = $this->brands->first();
|
||||
$perfume = Perfume::factory()->create([
|
||||
'name' => 'Test Perfume',
|
||||
'sku' => 'TP001',
|
||||
'brand_id' => $brand->id,
|
||||
'cost_price' => 150000,
|
||||
'sale_price' => 250000,
|
||||
'concentration' => Concentration::EAU_DE_PERFUME,
|
||||
'base_notes' => 'Vanilla, Musk',
|
||||
'middle_notes' => 'Jasmine, Rose',
|
||||
'top_notes' => 'Bergamot, Lemon',
|
||||
'description' => 'Test description',
|
||||
]);
|
||||
|
||||
$perfume->categories()->attach([$this->categories->first()->id]);
|
||||
$perfume->outlets()->attach([$this->outlets->first()->id]);
|
||||
|
||||
$component = mountPerfumeEditComponent($this->user, $perfume);
|
||||
|
||||
expect($component->form->name)->toBe('Test Perfume');
|
||||
expect($component->form->sku)->toBe('TP001');
|
||||
expect($component->form->brand_id)->toBe((string) $brand->id);
|
||||
expect($component->form->cost_price)->toBe('150000');
|
||||
expect($component->form->sale_price)->toBe('250000');
|
||||
expect($component->form->concentration)->toBe((string) Concentration::EAU_DE_PERFUME->value);
|
||||
expect($component->form->base_notes)->toBe('Vanilla, Musk');
|
||||
expect($component->form->middle_notes)->toBe('Jasmine, Rose');
|
||||
expect($component->form->top_notes)->toBe('Bergamot, Lemon');
|
||||
expect($component->form->description)->toBe('Test description');
|
||||
expect($component->form->category_ids)->toBe([$this->categories->first()->id]);
|
||||
expect($component->form->outlet_ids)->toBe([$this->outlets->first()->id]);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Update Perfume Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('updates perfume successfully when authorized', function () {
|
||||
$updatePermission = Permission::where('name', 'update perfume')->first();
|
||||
$this->user->givePermissionTo($updatePermission);
|
||||
|
||||
$oldBrand = $this->brands->first();
|
||||
$perfume = Perfume::factory()->create([
|
||||
'name' => 'Original Perfume',
|
||||
'sku' => 'OP001',
|
||||
'brand_id' => $oldBrand->id,
|
||||
'cost_price' => 100000,
|
||||
'sale_price' => 200000,
|
||||
'concentration' => Concentration::EAU_DE_TOILETTE,
|
||||
]);
|
||||
|
||||
$perfume->categories()->attach([$this->categories->first()->id]);
|
||||
$perfume->outlets()->attach([$this->outlets->first()->id]);
|
||||
|
||||
$newBrand = $this->brands->last();
|
||||
$updateData = [
|
||||
'name' => 'Updated Perfume',
|
||||
'sku' => 'UP001',
|
||||
'brand_id' => $newBrand->id,
|
||||
'cost_price' => '150000',
|
||||
'sale_price' => '300000',
|
||||
'concentration' => Concentration::EAU_DE_PERFUME->value,
|
||||
'base_notes' => 'Updated Base Notes',
|
||||
'middle_notes' => 'Updated Middle Notes',
|
||||
'top_notes' => 'Updated Top Notes',
|
||||
'description' => 'Updated description',
|
||||
'category_ids' => [$this->categories->first()->id, $this->categories->last()->id],
|
||||
'outlet_ids' => [$this->outlets->first()->id, $this->outlets->last()->id],
|
||||
];
|
||||
|
||||
mountPerfumeEditComponent($this->user, $perfume)
|
||||
->set('form.name', $updateData['name'])
|
||||
->set('form.sku', $updateData['sku'])
|
||||
->set('form.brand_id', $updateData['brand_id'])
|
||||
->set('form.cost_price', $updateData['cost_price'])
|
||||
->set('form.sale_price', $updateData['sale_price'])
|
||||
->set('form.concentration', $updateData['concentration'])
|
||||
->set('form.base_notes', $updateData['base_notes'])
|
||||
->set('form.middle_notes', $updateData['middle_notes'])
|
||||
->set('form.top_notes', $updateData['top_notes'])
|
||||
->set('form.description', $updateData['description'])
|
||||
->set('form.category_ids', $updateData['category_ids'])
|
||||
->set('form.outlet_ids', $updateData['outlet_ids'])
|
||||
->call('save')
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect(route('studio.catalog.perfume.index'));
|
||||
|
||||
$perfume->refresh();
|
||||
expect($perfume->name)->toBe($updateData['name']);
|
||||
expect($perfume->sku)->toBe($updateData['sku']);
|
||||
expect($perfume->brand_id)->toBe($updateData['brand_id']);
|
||||
expect($perfume->cost_price)->toBe(150000);
|
||||
expect($perfume->sale_price)->toBe(300000);
|
||||
expect($perfume->concentration->value)->toBe($updateData['concentration']);
|
||||
expect($perfume->base_notes)->toBe($updateData['base_notes']);
|
||||
expect($perfume->middle_notes)->toBe($updateData['middle_notes']);
|
||||
expect($perfume->top_notes)->toBe($updateData['top_notes']);
|
||||
expect($perfume->description)->toBe($updateData['description']);
|
||||
expect($perfume->categories)->toHaveCount(2);
|
||||
expect($perfume->outlets)->toHaveCount(2);
|
||||
});
|
||||
|
||||
it('prevents perfume update when unauthorized', function () {
|
||||
$this->ownerRole->revokePermissionTo('update perfume');
|
||||
Gate::define('update perfume', fn () => false);
|
||||
|
||||
$perfume = Perfume::factory()->create();
|
||||
|
||||
mountPerfumeEditComponent($this->user, $perfume)
|
||||
->set('form.name', 'Updated Name')
|
||||
->call('save')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('syncs perfume category and outlet relationships correctly on update', function () {
|
||||
$updatePermission = Permission::where('name', 'update perfume')->first();
|
||||
$this->user->givePermissionTo($updatePermission);
|
||||
|
||||
$perfume = Perfume::factory()->create();
|
||||
$perfume->categories()->attach([$this->categories->first()->id]);
|
||||
$perfume->outlets()->attach([$this->outlets->first()->id]);
|
||||
|
||||
// Change to different categories and outlets
|
||||
mountPerfumeEditComponent($this->user, $perfume)
|
||||
->set('form.name', 'Updated Perfume')
|
||||
->set('form.sku', 'UP001')
|
||||
->set('form.sale_price', '200000')
|
||||
->set('form.category_ids', [$this->categories[1]->id, $this->categories[2]->id])
|
||||
->set('form.outlet_ids', [$this->outlets[1]->id, $this->outlets[2]->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$perfume->refresh();
|
||||
expect($perfume->categories)->toHaveCount(2);
|
||||
expect($perfume->outlets)->toHaveCount(2);
|
||||
expect($perfume->categories->pluck('id')->toArray())->toBe([$this->categories[1]->id, $this->categories[2]->id]);
|
||||
expect($perfume->outlets->pluck('id')->toArray())->toBe([$this->outlets[1]->id, $this->outlets[2]->id]);
|
||||
});
|
||||
|
||||
it('allows same sku on update of same perfume', function () {
|
||||
$updatePermission = Permission::where('name', 'update perfume')->first();
|
||||
$this->user->givePermissionTo($updatePermission);
|
||||
|
||||
$perfume = Perfume::factory()->create(['sku' => 'EXISTING001']);
|
||||
|
||||
mountPerfumeEditComponent($this->user, $perfume)
|
||||
->set('form.name', 'Updated Perfume')
|
||||
->set('form.sku', 'EXISTING001') // Same SKU as existing perfume
|
||||
->set('form.sale_price', '200000')
|
||||
->set('form.category_ids', [$this->categories->first()->id])
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
|
||||
it('updates perfume brand to null', function () {
|
||||
$updatePermission = Permission::where('name', 'update perfume')->first();
|
||||
$this->user->givePermissionTo($updatePermission);
|
||||
|
||||
$perfume = Perfume::factory()->create(['brand_id' => $this->brands->first()->id]);
|
||||
|
||||
mountPerfumeEditComponent($this->user, $perfume)
|
||||
->set('form.name', 'Perfume Without Brand')
|
||||
->set('form.sku', 'PWB001')
|
||||
->set('form.brand_id', null) // Remove brand
|
||||
->set('form.sale_price', '150000')
|
||||
->set('form.category_ids', [$this->categories->first()->id])
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$perfume->refresh();
|
||||
expect($perfume->brand_id)->toBeNull();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Form Validation Tests on Edit
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates required name field on perfume update', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
|
||||
mountPerfumeEditComponent($this->user, $perfume)
|
||||
->set('form.name', '')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates name field max length on perfume update', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
|
||||
mountPerfumeEditComponent($this->user, $perfume)
|
||||
->set('form.name', str_repeat('a', 51))
|
||||
->call('save')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates required sku field on perfume update', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
|
||||
mountPerfumeEditComponent($this->user, $perfume)
|
||||
->set('form.sku', '')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.sku']);
|
||||
});
|
||||
|
||||
it('validates sku field max length on perfume update', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
|
||||
mountPerfumeEditComponent($this->user, $perfume)
|
||||
->set('form.sku', str_repeat('a', 21))
|
||||
->call('save')
|
||||
->assertHasErrors(['form.sku']);
|
||||
});
|
||||
|
||||
it('validates sku uniqueness on perfume update with different perfume', function () {
|
||||
$perfume1 = Perfume::factory()->create(['sku' => 'SKU001']);
|
||||
$perfume2 = Perfume::factory()->create(['sku' => 'SKU002']);
|
||||
|
||||
mountPerfumeEditComponent($this->user, $perfume1)
|
||||
->set('form.sku', 'SKU002') // Try to use SKU from perfume2
|
||||
->call('save')
|
||||
->assertHasErrors(['form.sku']);
|
||||
});
|
||||
|
||||
it('validates brand_id exists if provided on perfume update', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
|
||||
mountPerfumeEditComponent($this->user, $perfume)
|
||||
->set('form.brand_id', 999) // Non-existent brand
|
||||
->call('save')
|
||||
->assertHasErrors(['form.brand_id']);
|
||||
});
|
||||
|
||||
it('validates required sale_price field on perfume update', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
|
||||
mountPerfumeEditComponent($this->user, $perfume)
|
||||
->set('form.sale_price', '')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.sale_price']);
|
||||
});
|
||||
|
||||
it('validates concentration is valid enum value on perfume update', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
|
||||
mountPerfumeEditComponent($this->user, $perfume)
|
||||
->set('form.concentration', 'invalid')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.concentration']);
|
||||
});
|
||||
|
||||
it('validates required category_ids field on perfume update', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
|
||||
mountPerfumeEditComponent($this->user, $perfume)
|
||||
->set('form.category_ids', [])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.category_ids']);
|
||||
});
|
||||
|
||||
it('validates category_ids contains valid category ids on perfume update', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
|
||||
mountPerfumeEditComponent($this->user, $perfume)
|
||||
->set('form.category_ids', [999])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.category_ids.*']);
|
||||
});
|
||||
|
||||
it('validates required outlet_ids field on perfume update', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
|
||||
mountPerfumeEditComponent($this->user, $perfume)
|
||||
->set('form.outlet_ids', [])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.outlet_ids']);
|
||||
});
|
||||
|
||||
it('validates outlet_ids contains valid outlet ids on perfume update', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
|
||||
mountPerfumeEditComponent($this->user, $perfume)
|
||||
->set('form.outlet_ids', [999])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.outlet_ids.*']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Real-time Validation Tests on Edit
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates perfume name field in real-time on edit', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
|
||||
$component = mountPerfumeEditComponent($this->user, $perfume);
|
||||
|
||||
// Valid name
|
||||
$component->set('form.name', 'Valid Updated Perfume Name')
|
||||
->assertHasNoErrors(['form.name']);
|
||||
|
||||
// Empty name
|
||||
$component->set('form.name', '')
|
||||
->assertHasErrors(['form.name']);
|
||||
|
||||
// Name too long
|
||||
$component->set('form.name', str_repeat('a', 51))
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates perfume sku field in real-time on edit', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
|
||||
$component = mountPerfumeEditComponent($this->user, $perfume);
|
||||
|
||||
// Valid SKU
|
||||
$component->set('form.sku', 'VALID001')
|
||||
->assertHasNoErrors(['form.sku']);
|
||||
|
||||
// Empty SKU
|
||||
$component->set('form.sku', '')
|
||||
->assertHasErrors(['form.sku']);
|
||||
|
||||
// SKU too long
|
||||
$component->set('form.sku', str_repeat('a', 21))
|
||||
->assertHasErrors(['form.sku']);
|
||||
});
|
||||
|
||||
it('validates perfume price fields in real-time on edit', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
|
||||
$component = mountPerfumeEditComponent($this->user, $perfume);
|
||||
|
||||
// Valid sale price
|
||||
$component->set('form.sale_price', '300000')
|
||||
->assertHasNoErrors(['form.sale_price']);
|
||||
|
||||
// Empty sale price
|
||||
$component->set('form.sale_price', '')
|
||||
->assertHasErrors(['form.sale_price']);
|
||||
|
||||
// Negative sale price
|
||||
$component->set('form.sale_price', '-300000')
|
||||
->assertHasErrors(['form.sale_price']);
|
||||
});
|
||||
|
||||
it('validates perfume concentration field in real-time on edit', function () {
|
||||
$perfume = Perfume::factory()->create();
|
||||
|
||||
$component = mountPerfumeEditComponent($this->user, $perfume);
|
||||
|
||||
// Valid concentration
|
||||
$component->set('form.concentration', Concentration::EAU_DE_PERFUME->value)
|
||||
->assertHasNoErrors(['form.concentration']);
|
||||
|
||||
// Invalid concentration
|
||||
$component->set('form.concentration', 'invalid')
|
||||
->assertHasErrors(['form.concentration']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Integration Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('updates perfume and syncs category and outlet relationships', function () {
|
||||
$updatePermission = Permission::where('name', 'update perfume')->first();
|
||||
$this->user->givePermissionTo($updatePermission);
|
||||
|
||||
$perfume = Perfume::factory()->create();
|
||||
$perfume->categories()->attach([$this->categories->first()->id]);
|
||||
$perfume->outlets()->attach([$this->outlets->first()->id]);
|
||||
|
||||
$newCategories = [$this->categories[1]->id, $this->categories[2]->id];
|
||||
$newOutlets = [$this->outlets[1]->id, $this->outlets[2]->id];
|
||||
|
||||
mountPerfumeEditComponent($this->user, $perfume)
|
||||
->set('form.name', 'Updated Integration Perfume')
|
||||
->set('form.sku', 'UIP001')
|
||||
->set('form.sale_price', '400000')
|
||||
->set('form.category_ids', $newCategories)
|
||||
->set('form.outlet_ids', $newOutlets)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$perfume->refresh();
|
||||
expect($perfume->name)->toBe('Updated Integration Perfume');
|
||||
expect($perfume->categories)->toHaveCount(2);
|
||||
expect($perfume->outlets)->toHaveCount(2);
|
||||
expect($perfume->categories->pluck('id')->toArray())->toBe($newCategories);
|
||||
expect($perfume->outlets->pluck('id')->toArray())->toBe($newOutlets);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Error Handling and Edge Cases
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('handles invalid perfume hash in edit component', function () {
|
||||
expect(fn () => mountPerfumeEditComponent($this->user, 'invalid-hash'))
|
||||
->toThrow(\TypeError::class);
|
||||
});
|
||||
|
||||
it('validates perfume form data integrity during operations', function () {
|
||||
// Test that form data is properly reset between operations
|
||||
$perfume1 = Perfume::factory()->create(['name' => 'First Perfume']);
|
||||
$perfume2 = Perfume::factory()->create(['name' => 'Second Perfume']);
|
||||
|
||||
$component = mountPerfumeEditComponent($this->user, $perfume1);
|
||||
|
||||
// Open modal for first perfume
|
||||
expect($component->form->name)->toBe('First Perfume');
|
||||
|
||||
// Verify first perfume wasn't modified
|
||||
$perfume1->refresh();
|
||||
expect($perfume1->name)->toBe('First Perfume');
|
||||
});
|
||||
@ -1,118 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Employee;
|
||||
use App\Models\Perfume;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
|
||||
$this->ownerRole = Role::where('name', 'Owner')->first();
|
||||
$this->user->roles()->attach($this->ownerRole->id);
|
||||
});
|
||||
|
||||
function mountPerfumeIndexComponent(User $user)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(\App\Livewire\Studio\Catalog\Perfume\Index::class);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Index Component Rendering & Basic Functionality
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders perfume index page successfully', function () {
|
||||
mountPerfumeIndexComponent($this->user)
|
||||
->assertViewIs('livewire.studio.catalog.perfume.index')
|
||||
->assertViewHas('pageTitle', 'Parfum');
|
||||
});
|
||||
|
||||
it('displays perfumes correctly', function () {
|
||||
Perfume::factory()->count(3)->create();
|
||||
|
||||
// Just check that the component renders without errors and has perfumes
|
||||
$component = mountPerfumeIndexComponent($this->user);
|
||||
$component->assertViewIs('livewire.studio.catalog.perfume.index');
|
||||
expect(Perfume::count())->toBe(3);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Delete Perfume Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('deletes perfume successfully when authorized', function () {
|
||||
$perfume = Perfume::factory()->create([
|
||||
'name' => 'Perfume to Delete',
|
||||
]);
|
||||
|
||||
mountPerfumeIndexComponent($this->user)
|
||||
->call('delete', $perfume)
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
$this->assertSoftDeleted('perfumes', [
|
||||
'id' => $perfume->id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('prevents perfume deletion when unauthorized', function () {
|
||||
$this->ownerRole->revokePermissionTo('delete perfume');
|
||||
|
||||
$perfume = Perfume::factory()->create();
|
||||
|
||||
mountPerfumeIndexComponent($this->user)
|
||||
->call('delete', $perfume)
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authorization Edge Cases
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('handles unauthorized user with revoked permissions', function () {
|
||||
// Revoke all perfume permissions
|
||||
$this->ownerRole->revokePermissionTo(['delete perfume']);
|
||||
|
||||
// Define gates to return false
|
||||
Gate::define('delete perfume', fn () => false);
|
||||
|
||||
$perfume = Perfume::factory()->create();
|
||||
|
||||
// Test delete
|
||||
mountPerfumeIndexComponent($this->user)
|
||||
->call('delete', $perfume)
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('handles user with partial permissions correctly', function () {
|
||||
// Remove owner role and create a user with limited permissions
|
||||
$this->user->roles()->detach();
|
||||
$limitedRole = Role::create(['name' => 'Limited User']);
|
||||
$this->user->roles()->attach($limitedRole->id);
|
||||
|
||||
// Give only some permissions but not delete
|
||||
$limitedRole->givePermissionTo([]);
|
||||
|
||||
$perfume = Perfume::factory()->create();
|
||||
|
||||
// Should not be able to delete
|
||||
mountPerfumeIndexComponent($this->user)
|
||||
->call('delete', $perfume)
|
||||
->assertForbidden();
|
||||
});
|
||||
@ -1,473 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\AuditStockStatus;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\Product;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
|
||||
$this->ownerRole = Role::where('name', 'Owner')->first();
|
||||
$this->user->roles()->attach($this->ownerRole->id);
|
||||
|
||||
// Create test outlets
|
||||
$this->outlets = Outlet::factory()->count(3)->create();
|
||||
});
|
||||
|
||||
function mountProductAuditComponent(User $user, Outlet $outlet, Product $product)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(\App\Livewire\Studio\Catalog\Product\Audit::class, ['outlet' => $outlet, 'product' => $product]);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Audit Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders audit page successfully', function () {
|
||||
$product = Product::factory()->create();
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
mountProductAuditComponent($this->user, $outlet, $product)
|
||||
->assertViewIs('components.sections.ui.audit')
|
||||
->assertViewHas('pageTitle', "Audit {$product->name} di {$outlet->name}")
|
||||
->assertViewHas('module', 'product');
|
||||
});
|
||||
|
||||
it('sets label correctly on mount', function () {
|
||||
$product = Product::factory()->create(['name' => 'Test Product']);
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
$component = mountProductAuditComponent($this->user, $outlet, $product);
|
||||
|
||||
expect($component->label)->toBe('Test Product di '.$outlet->name);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Activity Logs Display Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('displays activity logs correctly', function () {
|
||||
$product = Product::factory()->create();
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
// Create some activity logs with expected properties
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock updated',
|
||||
'subject_type' => get_class($product),
|
||||
'subject_id' => $product->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 5,
|
||||
'quantity_change' => 10,
|
||||
'new_stock' => 15,
|
||||
],
|
||||
'event' => AuditStockStatus::INCREASED->value,
|
||||
]);
|
||||
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock decreased',
|
||||
'subject_type' => get_class($product),
|
||||
'subject_id' => $product->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 15,
|
||||
'quantity_change' => -5,
|
||||
'new_stock' => 10,
|
||||
],
|
||||
'event' => AuditStockStatus::DECREASED->value,
|
||||
]);
|
||||
|
||||
$component = mountProductAuditComponent($this->user, $outlet, $product);
|
||||
|
||||
expect($component->activityLogs)->toHaveCount(2);
|
||||
expect($component->activityLogs->first()->event['label'])->toBe(AuditStockStatus::INCREASED->label());
|
||||
expect($component->activityLogs->first()->event['color'])->toBe(AuditStockStatus::INCREASED->color());
|
||||
expect($component->activityLogs->last()->event['label'])->toBe(AuditStockStatus::DECREASED->label());
|
||||
expect($component->activityLogs->last()->event['color'])->toBe(AuditStockStatus::DECREASED->color());
|
||||
});
|
||||
|
||||
it('filters activity logs by outlet', function () {
|
||||
$product = Product::factory()->create();
|
||||
$outlet1 = $this->outlets->first();
|
||||
$outlet2 = $this->outlets->last();
|
||||
|
||||
// Create activity for outlet1
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock updated outlet1',
|
||||
'subject_type' => get_class($product),
|
||||
'subject_id' => $product->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet1->id,
|
||||
'previous_stock' => 5,
|
||||
'quantity_change' => 10,
|
||||
'new_stock' => 15,
|
||||
],
|
||||
'event' => AuditStockStatus::UPDATED->value,
|
||||
]);
|
||||
|
||||
// Create activity for outlet2
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock increased outlet2',
|
||||
'subject_type' => get_class($product),
|
||||
'subject_id' => $product->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet2->id,
|
||||
'previous_stock' => 0,
|
||||
'quantity_change' => 20,
|
||||
'new_stock' => 20,
|
||||
],
|
||||
'event' => AuditStockStatus::INCREASED->value,
|
||||
]);
|
||||
|
||||
$component1 = mountProductAuditComponent($this->user, $outlet1, $product);
|
||||
$component2 = mountProductAuditComponent($this->user, $outlet2, $product);
|
||||
|
||||
expect($component1->activityLogs)->toHaveCount(1);
|
||||
expect($component1->activityLogs->first()->properties['outlet_id'])->toBe($outlet1->id);
|
||||
|
||||
expect($component2->activityLogs)->toHaveCount(1);
|
||||
expect($component2->activityLogs->first()->properties['outlet_id'])->toBe($outlet2->id);
|
||||
});
|
||||
|
||||
it('orders activity logs by latest first', function () {
|
||||
$product = Product::factory()->create();
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
// Create activities with different timestamps
|
||||
$activity1 = Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'first activity',
|
||||
'subject_type' => get_class($product),
|
||||
'subject_id' => $product->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 5,
|
||||
'quantity_change' => 5,
|
||||
'new_stock' => 10,
|
||||
],
|
||||
'event' => AuditStockStatus::UPDATED->value,
|
||||
'created_at' => now()->subMinutes(5),
|
||||
]);
|
||||
|
||||
$activity2 = Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'second activity',
|
||||
'subject_type' => get_class($product),
|
||||
'subject_id' => $product->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 10,
|
||||
'quantity_change' => 10,
|
||||
'new_stock' => 20,
|
||||
],
|
||||
'event' => AuditStockStatus::INCREASED->value,
|
||||
'created_at' => now(),
|
||||
]);
|
||||
|
||||
$component = mountProductAuditComponent($this->user, $outlet, $product);
|
||||
|
||||
expect($component->activityLogs)->toHaveCount(2);
|
||||
expect($component->activityLogs->first()->id)->toBe($activity2->id);
|
||||
expect($component->activityLogs->last()->id)->toBe($activity1->id);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Activity Logs Content Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('maps activity events to correct labels and colors', function () {
|
||||
$product = Product::factory()->create();
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
// Test INCREASED event
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock increased',
|
||||
'subject_type' => get_class($product),
|
||||
'subject_id' => $product->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 10,
|
||||
'quantity_change' => 5,
|
||||
'new_stock' => 15,
|
||||
],
|
||||
'event' => AuditStockStatus::INCREASED->value,
|
||||
]);
|
||||
|
||||
// Test DECREASED event
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock decreased',
|
||||
'subject_type' => get_class($product),
|
||||
'subject_id' => $product->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 15,
|
||||
'quantity_change' => -3,
|
||||
'new_stock' => 12,
|
||||
],
|
||||
'event' => AuditStockStatus::DECREASED->value,
|
||||
]);
|
||||
|
||||
// Test UPDATED event
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock updated',
|
||||
'subject_type' => get_class($product),
|
||||
'subject_id' => $product->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 12,
|
||||
'quantity_change' => 0,
|
||||
'new_stock' => 12,
|
||||
],
|
||||
'event' => AuditStockStatus::UPDATED->value,
|
||||
]);
|
||||
|
||||
$component = mountProductAuditComponent($this->user, $outlet, $product);
|
||||
|
||||
expect($component->activityLogs)->toHaveCount(3);
|
||||
|
||||
// Check all event mappings
|
||||
foreach ($component->activityLogs as $log) {
|
||||
match ($log->event['label']) {
|
||||
AuditStockStatus::INCREASED->label() => expect($log->event['color'])->toBe(AuditStockStatus::INCREASED->color()),
|
||||
AuditStockStatus::DECREASED->label() => expect($log->event['color'])->toBe(AuditStockStatus::DECREASED->color()),
|
||||
AuditStockStatus::UPDATED->label() => expect($log->event['color'])->toBe(AuditStockStatus::UPDATED->color()),
|
||||
default => throw new \Exception("Unexpected event label: {$log->event['label']}")
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Empty State Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('handles empty activity logs in audit component', function () {
|
||||
$product = Product::factory()->create();
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
$component = mountProductAuditComponent($this->user, $outlet, $product);
|
||||
|
||||
expect($component->activityLogs)->toBeInstanceOf(\Illuminate\Database\Eloquent\Collection::class);
|
||||
expect($component->activityLogs)->toHaveCount(0);
|
||||
});
|
||||
|
||||
it('handles product with no activities in specific outlet', function () {
|
||||
$product = Product::factory()->create();
|
||||
$outlet1 = $this->outlets->first();
|
||||
$outlet2 = $this->outlets->last();
|
||||
|
||||
// Create activity only for outlet2
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock updated',
|
||||
'subject_type' => get_class($product),
|
||||
'subject_id' => $product->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet2->id,
|
||||
'previous_stock' => 5,
|
||||
'quantity_change' => 5,
|
||||
'new_stock' => 10,
|
||||
],
|
||||
'event' => AuditStockStatus::UPDATED->value,
|
||||
]);
|
||||
|
||||
$component1 = mountProductAuditComponent($this->user, $outlet1, $product);
|
||||
$component2 = mountProductAuditComponent($this->user, $outlet2, $product);
|
||||
|
||||
expect($component1->activityLogs)->toHaveCount(0);
|
||||
expect($component2->activityLogs)->toHaveCount(1);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Integration Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('displays real activity log data correctly', function () {
|
||||
$product = Product::factory()->create(['name' => 'Integration Product']);
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
$activity = Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Stock adjustment for integration test',
|
||||
'subject_type' => get_class($product),
|
||||
'subject_id' => $product->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 100,
|
||||
'quantity_change' => 25,
|
||||
'new_stock' => 125,
|
||||
],
|
||||
'event' => AuditStockStatus::INCREASED->value,
|
||||
]);
|
||||
|
||||
$component = mountProductAuditComponent($this->user, $outlet, $product);
|
||||
|
||||
expect($component->activityLogs)->toHaveCount(1);
|
||||
|
||||
$log = $component->activityLogs->first();
|
||||
expect($log->description)->toBe('Stock adjustment for integration test');
|
||||
expect($log->properties['previous_stock'])->toBe(100);
|
||||
expect($log->properties['quantity_change'])->toBe(25);
|
||||
expect($log->properties['new_stock'])->toBe(125);
|
||||
expect($log->event['label'])->toBe(AuditStockStatus::INCREASED->label());
|
||||
expect($log->event['color'])->toBe(AuditStockStatus::INCREASED->color());
|
||||
});
|
||||
|
||||
it('handles multiple activities across different outlets', function () {
|
||||
$product = Product::factory()->create();
|
||||
$outlet1 = $this->outlets->first();
|
||||
$outlet2 = $this->outlets->last();
|
||||
|
||||
// Create activities for both outlets
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Activity in outlet 1',
|
||||
'subject_type' => get_class($product),
|
||||
'subject_id' => $product->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet1->id,
|
||||
'previous_stock' => 10,
|
||||
'quantity_change' => 5,
|
||||
'new_stock' => 15,
|
||||
],
|
||||
'event' => AuditStockStatus::INCREASED->value,
|
||||
]);
|
||||
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Activity in outlet 2',
|
||||
'subject_type' => get_class($product),
|
||||
'subject_id' => $product->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet2->id,
|
||||
'previous_stock' => 20,
|
||||
'quantity_change' => -3,
|
||||
'new_stock' => 17,
|
||||
],
|
||||
'event' => AuditStockStatus::DECREASED->value,
|
||||
]);
|
||||
|
||||
$component1 = mountProductAuditComponent($this->user, $outlet1, $product);
|
||||
$component2 = mountProductAuditComponent($this->user, $outlet2, $product);
|
||||
|
||||
expect($component1->activityLogs)->toHaveCount(1);
|
||||
expect($component1->activityLogs->first()->description)->toBe('Activity in outlet 1');
|
||||
|
||||
expect($component2->activityLogs)->toHaveCount(1);
|
||||
expect($component2->activityLogs->first()->description)->toBe('Activity in outlet 2');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Edge Cases and Error Handling
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('handles activity logs with missing properties gracefully', function () {
|
||||
$product = Product::factory()->create();
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
// Create activity with incomplete properties (shouldn't happen in real usage)
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Incomplete activity',
|
||||
'subject_type' => get_class($product),
|
||||
'subject_id' => $product->id,
|
||||
'causer_type' => get_class($this->user),
|
||||
'causer_id' => $this->user->id,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 0, // Add required properties
|
||||
'quantity_change' => 0,
|
||||
'new_stock' => 0,
|
||||
],
|
||||
'event' => AuditStockStatus::UPDATED->value,
|
||||
]);
|
||||
|
||||
$component = mountProductAuditComponent($this->user, $outlet, $product);
|
||||
|
||||
expect($component->activityLogs)->toHaveCount(1);
|
||||
// Component should render correctly with all required properties
|
||||
});
|
||||
|
||||
it('handles activities with null causer', function () {
|
||||
$product = Product::factory()->create();
|
||||
$outlet = $this->outlets->first();
|
||||
|
||||
Activity::create([
|
||||
'log_name' => 'default',
|
||||
'description' => 'Activity with no causer',
|
||||
'subject_type' => get_class($product),
|
||||
'subject_id' => $product->id,
|
||||
'causer_type' => null,
|
||||
'causer_id' => null,
|
||||
'properties' => [
|
||||
'outlet_id' => $outlet->id,
|
||||
'previous_stock' => 5,
|
||||
'quantity_change' => 5,
|
||||
'new_stock' => 10,
|
||||
],
|
||||
'event' => AuditStockStatus::INCREASED->value,
|
||||
]);
|
||||
|
||||
$component = mountProductAuditComponent($this->user, $outlet, $product);
|
||||
|
||||
expect($component->activityLogs)->toHaveCount(1);
|
||||
expect($component->activityLogs->first()->causer)->toBeNull();
|
||||
});
|
||||
@ -1,351 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Employee;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\Product;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
|
||||
$this->ownerRole = Role::where('name', 'Owner')->first();
|
||||
$this->user->roles()->attach($this->ownerRole->id);
|
||||
|
||||
// Create test outlets
|
||||
$this->outlets = Outlet::factory()->count(3)->create();
|
||||
});
|
||||
|
||||
function mountProductCreateComponent(User $user)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(\App\Livewire\Studio\Catalog\Product\Create::class);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Create Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders create page successfully', function () {
|
||||
mountProductCreateComponent($this->user)
|
||||
->assertViewIs('livewire.studio.catalog.product.form')
|
||||
->assertViewHas('pageTitle', 'Tambah Produk');
|
||||
});
|
||||
|
||||
it('initializes form properties on mount', function () {
|
||||
$component = mountProductCreateComponent($this->user);
|
||||
|
||||
expect($component->form->name)->toBe('');
|
||||
expect($component->form->sku)->toBe('');
|
||||
expect($component->form->cost_price)->toBe('');
|
||||
expect($component->form->sale_price)->toBe('');
|
||||
expect($component->form->description)->toBeNull();
|
||||
expect($component->form->outlet_ids)->toBe([]);
|
||||
expect($component->form->image)->toBe([]);
|
||||
expect($component->outlets)->toHaveCount(3);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Create Product Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('creates product successfully when authorized', function () {
|
||||
$data = [
|
||||
'name' => 'New Product',
|
||||
'sku' => 'NP001',
|
||||
'cost_price' => '10000',
|
||||
'sale_price' => '15000',
|
||||
'description' => 'Product description',
|
||||
'outlet_ids' => [$this->outlets->first()->id, $this->outlets->last()->id],
|
||||
];
|
||||
|
||||
mountProductCreateComponent($this->user)
|
||||
->set('form.name', $data['name'])
|
||||
->set('form.sku', $data['sku'])
|
||||
->set('form.cost_price', $data['cost_price'])
|
||||
->set('form.sale_price', $data['sale_price'])
|
||||
->set('form.description', $data['description'])
|
||||
->set('form.outlet_ids', $data['outlet_ids'])
|
||||
->call('save')
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect(route('studio.catalog.product.index'));
|
||||
|
||||
expect(Product::count())->toBe(1);
|
||||
$product = Product::first();
|
||||
expect($product->name)->toBe($data['name']);
|
||||
expect($product->sku)->toBe($data['sku']);
|
||||
expect($product->cost_price)->toBe(10000);
|
||||
expect($product->sale_price)->toBe(15000);
|
||||
expect($product->description)->toBe($data['description']);
|
||||
expect($product->outlets)->toHaveCount(2);
|
||||
});
|
||||
|
||||
it('prevents product creation when unauthorized', function () {
|
||||
$this->ownerRole->revokePermissionTo('create product');
|
||||
Gate::define('create product', fn () => false);
|
||||
|
||||
mountProductCreateComponent($this->user)
|
||||
->set('form.name', 'Test Product')
|
||||
->set('form.sku', 'TP001')
|
||||
->set('form.sale_price', '10000')
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('creates product with empty cost_price for non-owner roles', function () {
|
||||
// Create user with limited role
|
||||
$this->user->roles()->detach();
|
||||
$limitedRole = Role::create(['name' => 'Cashier']);
|
||||
$this->user->roles()->attach($limitedRole->id);
|
||||
|
||||
$createPermission = Permission::where('name', 'create product')->first();
|
||||
$limitedRole->givePermissionTo($createPermission);
|
||||
|
||||
mountProductCreateComponent($this->user)
|
||||
->set('form.name', 'Test Product')
|
||||
->set('form.sku', 'TP001')
|
||||
->set('form.cost_price', '') // Empty cost price
|
||||
->set('form.sale_price', '10000')
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$product = Product::first();
|
||||
expect($product->cost_price)->toBe(0);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Form Validation Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates required name field on create', function () {
|
||||
mountProductCreateComponent($this->user)
|
||||
->set('form.name', '')
|
||||
->set('form.sku', 'TP001')
|
||||
->set('form.sale_price', '10000')
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates name field max length on create', function () {
|
||||
mountProductCreateComponent($this->user)
|
||||
->set('form.name', str_repeat('a', 51))
|
||||
->set('form.sku', 'TP001')
|
||||
->set('form.sale_price', '10000')
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates required sku field on create', function () {
|
||||
mountProductCreateComponent($this->user)
|
||||
->set('form.name', 'Test Product')
|
||||
->set('form.sku', '')
|
||||
->set('form.sale_price', '10000')
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.sku']);
|
||||
});
|
||||
|
||||
it('validates sku field max length on create', function () {
|
||||
mountProductCreateComponent($this->user)
|
||||
->set('form.name', 'Test Product')
|
||||
->set('form.sku', str_repeat('a', 21))
|
||||
->set('form.sale_price', '10000')
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.sku']);
|
||||
});
|
||||
|
||||
it('validates sku uniqueness on create', function () {
|
||||
Product::factory()->create(['sku' => 'EXISTING001']);
|
||||
|
||||
mountProductCreateComponent($this->user)
|
||||
->set('form.name', 'Test Product')
|
||||
->set('form.sku', 'EXISTING001')
|
||||
->set('form.sale_price', '10000')
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.sku']);
|
||||
});
|
||||
|
||||
it('validates required sale_price field on create', function () {
|
||||
mountProductCreateComponent($this->user)
|
||||
->set('form.name', 'Test Product')
|
||||
->set('form.sku', 'TP001')
|
||||
->set('form.sale_price', '')
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.sale_price']);
|
||||
});
|
||||
|
||||
it('validates required outlet_ids field on create', function () {
|
||||
mountProductCreateComponent($this->user)
|
||||
->set('form.name', 'Test Product')
|
||||
->set('form.sku', 'TP001')
|
||||
->set('form.sale_price', '10000')
|
||||
->set('form.outlet_ids', [])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.outlet_ids']);
|
||||
});
|
||||
|
||||
it('validates outlet_ids contains valid outlet ids on create', function () {
|
||||
mountProductCreateComponent($this->user)
|
||||
->set('form.name', 'Test Product')
|
||||
->set('form.sku', 'TP001')
|
||||
->set('form.sale_price', '10000')
|
||||
->set('form.outlet_ids', [999])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.outlet_ids.*']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Real-time Validation Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates name field in real-time', function () {
|
||||
$component = mountProductCreateComponent($this->user);
|
||||
|
||||
// Valid name
|
||||
$component->set('form.name', 'Valid Product Name')
|
||||
->assertHasNoErrors(['form.name']);
|
||||
|
||||
// Empty name
|
||||
$component->set('form.name', '')
|
||||
->assertHasErrors(['form.name']);
|
||||
|
||||
// Name too long
|
||||
$component->set('form.name', str_repeat('a', 51))
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates sku field in real-time', function () {
|
||||
$component = mountProductCreateComponent($this->user);
|
||||
|
||||
// Valid SKU
|
||||
$component->set('form.sku', 'VALID001')
|
||||
->assertHasNoErrors(['form.sku']);
|
||||
|
||||
// Empty SKU
|
||||
$component->set('form.sku', '')
|
||||
->assertHasErrors(['form.sku']);
|
||||
|
||||
// SKU too long
|
||||
$component->set('form.sku', str_repeat('a', 21))
|
||||
->assertHasErrors(['form.sku']);
|
||||
|
||||
// Duplicate SKU
|
||||
Product::factory()->create(['sku' => 'EXISTING001']);
|
||||
$component->set('form.sku', 'EXISTING001')
|
||||
->assertHasErrors(['form.sku']);
|
||||
});
|
||||
|
||||
it('validates price fields in real-time', function () {
|
||||
$component = mountProductCreateComponent($this->user);
|
||||
|
||||
// Valid sale price
|
||||
$component->set('form.sale_price', '10000')
|
||||
->assertHasNoErrors(['form.sale_price']);
|
||||
|
||||
// Empty sale price
|
||||
$component->set('form.sale_price', '')
|
||||
->assertHasErrors(['form.sale_price']);
|
||||
|
||||
// Negative sale price
|
||||
$component->set('form.sale_price', '-10000')
|
||||
->assertHasErrors(['form.sale_price']);
|
||||
|
||||
// Valid cost price
|
||||
$component->set('form.cost_price', '5000')
|
||||
->assertHasNoErrors(['form.cost_price']);
|
||||
|
||||
// Negative cost price
|
||||
$component->set('form.cost_price', '-5000')
|
||||
->assertHasErrors(['form.cost_price']);
|
||||
});
|
||||
|
||||
it('validates outlet_ids in real-time', function () {
|
||||
$component = mountProductCreateComponent($this->user);
|
||||
|
||||
// Valid outlet IDs
|
||||
$component->set('form.name', 'Test Product')
|
||||
->set('form.sku', 'TP001')
|
||||
->set('form.sale_price', '10000')
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->assertHasNoErrors(['form.outlet_ids']);
|
||||
|
||||
// Empty outlet IDs - should trigger validation on submit
|
||||
$component->set('form.outlet_ids', [])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.outlet_ids']);
|
||||
|
||||
// Invalid outlet ID
|
||||
$component = mountProductCreateComponent($this->user);
|
||||
$component->set('form.name', 'Test Product')
|
||||
->set('form.sku', 'TP002')
|
||||
->set('form.sale_price', '10000')
|
||||
->set('form.outlet_ids', [999])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.outlet_ids.*']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Integration Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('creates product and attaches to outlets correctly', function () {
|
||||
$outlet1 = $this->outlets->first();
|
||||
$outlet2 = $this->outlets->last();
|
||||
|
||||
mountProductCreateComponent($this->user)
|
||||
->set('form.name', 'Integration Test Product')
|
||||
->set('form.sku', 'ITP001')
|
||||
->set('form.cost_price', '15000') // Required for Owner role
|
||||
->set('form.sale_price', '20000')
|
||||
->set('form.outlet_ids', [$outlet1->id, $outlet2->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$product = Product::where('sku', 'ITP001')->first();
|
||||
expect($product)->not->toBeNull();
|
||||
expect($product->outlets)->toHaveCount(2);
|
||||
expect($product->outlets->pluck('id')->toArray())->toContain($outlet1->id, $outlet2->id);
|
||||
});
|
||||
|
||||
it('handles large numbers in price fields', function () {
|
||||
mountProductCreateComponent($this->user)
|
||||
->set('form.name', 'Expensive Product')
|
||||
->set('form.sku', 'EP001')
|
||||
->set('form.cost_price', '10000000') // 10 million
|
||||
->set('form.sale_price', '15000000') // 15 million
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$product = Product::where('sku', 'EP001')->first();
|
||||
expect($product->cost_price)->toBe(10000000);
|
||||
expect($product->sale_price)->toBe(15000000);
|
||||
});
|
||||
@ -1,359 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Employee;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\Product;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
|
||||
$this->ownerRole = Role::where('name', 'Owner')->first();
|
||||
$this->user->roles()->attach($this->ownerRole->id);
|
||||
|
||||
// Create test outlets
|
||||
$this->outlets = Outlet::factory()->count(3)->create();
|
||||
});
|
||||
|
||||
function mountProductEditComponent(User $user, Product $product)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(\App\Livewire\Studio\Catalog\Product\Edit::class, ['product' => $product->hash]);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Edit Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders edit page successfully', function () {
|
||||
$product = Product::factory()->create();
|
||||
|
||||
mountProductEditComponent($this->user, $product)
|
||||
->assertViewIs('livewire.studio.catalog.product.form')
|
||||
->assertViewHas('pageTitle', 'Ubah Produk');
|
||||
});
|
||||
|
||||
it('populates form with product data on mount', function () {
|
||||
$product = Product::factory()->create([
|
||||
'name' => 'Test Product',
|
||||
'sku' => 'TP001',
|
||||
'cost_price' => 10000,
|
||||
'sale_price' => 15000,
|
||||
'description' => 'Test description',
|
||||
]);
|
||||
|
||||
$product->outlets()->attach([$this->outlets->first()->id]);
|
||||
|
||||
$component = mountProductEditComponent($this->user, $product);
|
||||
|
||||
expect($component->form->name)->toBe('Test Product');
|
||||
expect($component->form->sku)->toBe('TP001');
|
||||
expect($component->form->cost_price)->toBe('10000');
|
||||
expect($component->form->sale_price)->toBe('15000');
|
||||
expect($component->form->description)->toBe('Test description');
|
||||
expect($component->form->outlet_ids)->toBe([$this->outlets->first()->id]);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Update Product Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('updates product successfully when authorized', function () {
|
||||
$updatePermission = Permission::where('name', 'update product')->first();
|
||||
$this->user->givePermissionTo($updatePermission);
|
||||
|
||||
$product = Product::factory()->create([
|
||||
'name' => 'Original Product',
|
||||
'sku' => 'OP001',
|
||||
'cost_price' => 10000,
|
||||
'sale_price' => 15000,
|
||||
]);
|
||||
|
||||
$product->outlets()->attach([$this->outlets->first()->id]);
|
||||
|
||||
$updateData = [
|
||||
'name' => 'Updated Product',
|
||||
'sku' => 'UP001',
|
||||
'cost_price' => '12000',
|
||||
'sale_price' => '18000',
|
||||
'description' => 'Updated description',
|
||||
'outlet_ids' => [$this->outlets->first()->id, $this->outlets->last()->id],
|
||||
];
|
||||
|
||||
mountProductEditComponent($this->user, $product)
|
||||
->set('form.name', $updateData['name'])
|
||||
->set('form.sku', $updateData['sku'])
|
||||
->set('form.cost_price', $updateData['cost_price'])
|
||||
->set('form.sale_price', $updateData['sale_price'])
|
||||
->set('form.description', $updateData['description'])
|
||||
->set('form.outlet_ids', $updateData['outlet_ids'])
|
||||
->call('save')
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect(route('studio.catalog.product.index'));
|
||||
|
||||
$product->refresh();
|
||||
expect($product->name)->toBe($updateData['name']);
|
||||
expect($product->sku)->toBe($updateData['sku']);
|
||||
expect($product->cost_price)->toBe(12000);
|
||||
expect($product->sale_price)->toBe(18000);
|
||||
expect($product->description)->toBe($updateData['description']);
|
||||
expect($product->outlets)->toHaveCount(2);
|
||||
});
|
||||
|
||||
it('prevents product update when unauthorized', function () {
|
||||
$this->ownerRole->revokePermissionTo('update product');
|
||||
Gate::define('update product', fn () => false);
|
||||
|
||||
$product = Product::factory()->create();
|
||||
|
||||
mountProductEditComponent($this->user, $product)
|
||||
->set('form.name', 'Updated Name')
|
||||
->call('save')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('syncs outlet relationships correctly on update', function () {
|
||||
$updatePermission = Permission::where('name', 'update product')->first();
|
||||
$this->user->givePermissionTo($updatePermission);
|
||||
|
||||
$product = Product::factory()->create();
|
||||
$product->outlets()->attach([$this->outlets->first()->id]);
|
||||
|
||||
// Change to different outlets
|
||||
mountProductEditComponent($this->user, $product)
|
||||
->set('form.name', 'Updated Product')
|
||||
->set('form.sku', 'UP001')
|
||||
->set('form.sale_price', '10000')
|
||||
->set('form.outlet_ids', [$this->outlets[1]->id, $this->outlets[2]->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$product->refresh();
|
||||
expect($product->outlets)->toHaveCount(2);
|
||||
expect($product->outlets->pluck('id')->toArray())->toBe([$this->outlets[1]->id, $this->outlets[2]->id]);
|
||||
});
|
||||
|
||||
it('allows same sku on update of same product', function () {
|
||||
$updatePermission = Permission::where('name', 'update product')->first();
|
||||
$this->user->givePermissionTo($updatePermission);
|
||||
|
||||
$product = Product::factory()->create(['sku' => 'EXISTING001']);
|
||||
|
||||
mountProductEditComponent($this->user, $product)
|
||||
->set('form.name', 'Updated Product')
|
||||
->set('form.sku', 'EXISTING001') // Same SKU as existing product
|
||||
->set('form.sale_price', '10000')
|
||||
->set('form.outlet_ids', [$this->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Form Validation Tests on Edit
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates required name field on update', function () {
|
||||
$product = Product::factory()->create();
|
||||
|
||||
mountProductEditComponent($this->user, $product)
|
||||
->set('form.name', '')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates name field max length on update', function () {
|
||||
$product = Product::factory()->create();
|
||||
|
||||
mountProductEditComponent($this->user, $product)
|
||||
->set('form.name', str_repeat('a', 51))
|
||||
->call('save')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates required sku field on update', function () {
|
||||
$product = Product::factory()->create();
|
||||
|
||||
mountProductEditComponent($this->user, $product)
|
||||
->set('form.sku', '')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.sku']);
|
||||
});
|
||||
|
||||
it('validates sku field max length on update', function () {
|
||||
$product = Product::factory()->create();
|
||||
|
||||
mountProductEditComponent($this->user, $product)
|
||||
->set('form.sku', str_repeat('a', 21))
|
||||
->call('save')
|
||||
->assertHasErrors(['form.sku']);
|
||||
});
|
||||
|
||||
it('validates sku uniqueness on update with different product', function () {
|
||||
$product1 = Product::factory()->create(['sku' => 'SKU001']);
|
||||
$product2 = Product::factory()->create(['sku' => 'SKU002']);
|
||||
|
||||
mountProductEditComponent($this->user, $product1)
|
||||
->set('form.sku', 'SKU002') // Try to use SKU from product2
|
||||
->call('save')
|
||||
->assertHasErrors(['form.sku']);
|
||||
});
|
||||
|
||||
it('validates required sale_price field on update', function () {
|
||||
$product = Product::factory()->create();
|
||||
|
||||
mountProductEditComponent($this->user, $product)
|
||||
->set('form.sale_price', '')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.sale_price']);
|
||||
});
|
||||
|
||||
it('validates required outlet_ids field on update', function () {
|
||||
$product = Product::factory()->create();
|
||||
|
||||
mountProductEditComponent($this->user, $product)
|
||||
->set('form.outlet_ids', [])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.outlet_ids']);
|
||||
});
|
||||
|
||||
it('validates outlet_ids contains valid outlet ids on update', function () {
|
||||
$product = Product::factory()->create();
|
||||
|
||||
mountProductEditComponent($this->user, $product)
|
||||
->set('form.outlet_ids', [999])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.outlet_ids.*']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Real-time Validation Tests on Edit
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates name field in real-time on edit', function () {
|
||||
$product = Product::factory()->create();
|
||||
|
||||
$component = mountProductEditComponent($this->user, $product);
|
||||
|
||||
// Valid name
|
||||
$component->set('form.name', 'Valid Updated Product Name')
|
||||
->assertHasNoErrors(['form.name']);
|
||||
|
||||
// Empty name
|
||||
$component->set('form.name', '')
|
||||
->assertHasErrors(['form.name']);
|
||||
|
||||
// Name too long
|
||||
$component->set('form.name', str_repeat('a', 51))
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates sku field in real-time on edit', function () {
|
||||
$product = Product::factory()->create();
|
||||
|
||||
$component = mountProductEditComponent($this->user, $product);
|
||||
|
||||
// Valid SKU
|
||||
$component->set('form.sku', 'VALID001')
|
||||
->assertHasNoErrors(['form.sku']);
|
||||
|
||||
// Empty SKU
|
||||
$component->set('form.sku', '')
|
||||
->assertHasErrors(['form.sku']);
|
||||
|
||||
// SKU too long
|
||||
$component->set('form.sku', str_repeat('a', 21))
|
||||
->assertHasErrors(['form.sku']);
|
||||
});
|
||||
|
||||
it('validates price fields in real-time on edit', function () {
|
||||
$product = Product::factory()->create();
|
||||
|
||||
$component = mountProductEditComponent($this->user, $product);
|
||||
|
||||
// Valid sale price
|
||||
$component->set('form.sale_price', '10000')
|
||||
->assertHasNoErrors(['form.sale_price']);
|
||||
|
||||
// Empty sale price
|
||||
$component->set('form.sale_price', '')
|
||||
->assertHasErrors(['form.sale_price']);
|
||||
|
||||
// Negative sale price
|
||||
$component->set('form.sale_price', '-10000')
|
||||
->assertHasErrors(['form.sale_price']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Integration Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('updates product and syncs outlet relationships', function () {
|
||||
$updatePermission = Permission::where('name', 'update product')->first();
|
||||
$this->user->givePermissionTo($updatePermission);
|
||||
|
||||
$product = Product::factory()->create();
|
||||
$product->outlets()->attach([$this->outlets->first()->id]);
|
||||
|
||||
$newOutlets = [$this->outlets[1]->id, $this->outlets[2]->id];
|
||||
|
||||
mountProductEditComponent($this->user, $product)
|
||||
->set('form.name', 'Updated Integration Product')
|
||||
->set('form.sku', 'UIP001')
|
||||
->set('form.sale_price', '25000')
|
||||
->set('form.outlet_ids', $newOutlets)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$product->refresh();
|
||||
expect($product->name)->toBe('Updated Integration Product');
|
||||
expect($product->outlets)->toHaveCount(2);
|
||||
expect($product->outlets->pluck('id')->toArray())->toBe($newOutlets);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Error Handling and Edge Cases
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('handles invalid product hash in edit component', function () {
|
||||
expect(fn () => mountProductEditComponent($this->user, 'invalid-hash'))
|
||||
->toThrow(\TypeError::class);
|
||||
});
|
||||
|
||||
it('validates form data integrity during operations', function () {
|
||||
// Test that form data is properly reset between operations
|
||||
$product1 = Product::factory()->create(['name' => 'First Category']);
|
||||
$product2 = Product::factory()->create(['name' => 'Second Category']);
|
||||
|
||||
$component = mountProductEditComponent($this->user, $product1);
|
||||
|
||||
// Open modal for first category
|
||||
expect($component->form->name)->toBe('First Category');
|
||||
|
||||
// Verify first category wasn't modified
|
||||
$product1->refresh();
|
||||
expect($product1->name)->toBe('First Category');
|
||||
});
|
||||
@ -1,120 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Employee;
|
||||
use App\Models\Product;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
|
||||
$this->ownerRole = Role::where('name', 'Owner')->first();
|
||||
$this->user->roles()->attach($this->ownerRole->id);
|
||||
});
|
||||
|
||||
function mountProductIndexComponent(User $user)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(\App\Livewire\Studio\Catalog\Product\Index::class);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Index Component Rendering & Basic Functionality
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders index page successfully', function () {
|
||||
mountProductIndexComponent($this->user)
|
||||
->assertViewIs('livewire.studio.catalog.product.index')
|
||||
->assertViewHas('pageTitle', 'Produk');
|
||||
});
|
||||
|
||||
it('displays products correctly', function () {
|
||||
Product::factory()->count(3)->create();
|
||||
|
||||
// Just check that the component renders without errors and has products
|
||||
$component = mountProductIndexComponent($this->user);
|
||||
$component->assertViewIs('livewire.studio.catalog.product.index');
|
||||
expect(Product::count())->toBe(3);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Delete Product Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('deletes product successfully when authorized', function () {
|
||||
$product = Product::factory()->create([
|
||||
'name' => 'Product to Delete',
|
||||
]);
|
||||
|
||||
mountProductIndexComponent($this->user)
|
||||
->call('delete', $product)
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
$this->assertSoftDeleted('products', [
|
||||
'id' => $product->id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('prevents product deletion when unauthorized', function () {
|
||||
$this->ownerRole->revokePermissionTo('delete product');
|
||||
|
||||
Gate::define('delete product', fn () => false);
|
||||
|
||||
$product = Product::factory()->create();
|
||||
|
||||
mountProductIndexComponent($this->user)
|
||||
->call('delete', $product)
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authorization Edge Cases
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('handles unauthorized user with revoked permissions', function () {
|
||||
// Revoke all product permissions
|
||||
$this->ownerRole->revokePermissionTo(['delete product']);
|
||||
|
||||
// Define gates to return false
|
||||
Gate::define('delete product', fn () => false);
|
||||
|
||||
$product = Product::factory()->create();
|
||||
|
||||
// Test delete
|
||||
mountProductIndexComponent($this->user)
|
||||
->call('delete', $product)
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('handles user with partial permissions correctly', function () {
|
||||
// Remove owner role and create a user with limited permissions
|
||||
$this->user->roles()->detach();
|
||||
$limitedRole = Role::create(['name' => 'Limited User']);
|
||||
$this->user->roles()->attach($limitedRole->id);
|
||||
|
||||
// Give only some permissions but not delete
|
||||
$limitedRole->givePermissionTo([]);
|
||||
|
||||
$product = Product::factory()->create();
|
||||
|
||||
// Should not be able to delete
|
||||
mountProductIndexComponent($this->user)
|
||||
->call('delete', $product)
|
||||
->assertForbidden();
|
||||
});
|
||||
@ -1,431 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Studio\Feature\ChooseUs;
|
||||
use App\Models\ChooseUs as ChooseUsModel;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->create();
|
||||
|
||||
$this->ownerRole = Role::where('name', 'Owner')->first();
|
||||
|
||||
$this->user->roles()->attach($this->ownerRole->id);
|
||||
});
|
||||
|
||||
function mountChooseUsComponent(User $user)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(ChooseUs::class);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders page successfully', function () {
|
||||
mountChooseUsComponent($this->user)
|
||||
->assertViewIs('livewire.studio.feature.choose-us')
|
||||
->assertViewHas('pageTitle', 'Pilih Kami');
|
||||
});
|
||||
|
||||
it('mounts with default method', function () {
|
||||
$component = mountChooseUsComponent($this->user);
|
||||
|
||||
expect($component->method)->toBe('create');
|
||||
expect($component->modalTitle)->toBe('');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Create Choose Us
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('creates choose us successfully', function () {
|
||||
mountChooseUsComponent($this->user)
|
||||
->set('form.name', 'Quality Products')
|
||||
->set('form.description', 'We provide high-quality products that meet your needs.')
|
||||
->call('create');
|
||||
|
||||
$chooseUs = ChooseUsModel::first();
|
||||
expect($chooseUs)->not->toBeNull();
|
||||
expect($chooseUs->name)->toBe('Quality Products');
|
||||
expect($chooseUs->description)->toBe('We provide high-quality products that meet your needs.');
|
||||
expect($chooseUs->sort_order)->toBe(1);
|
||||
});
|
||||
|
||||
it('validates required fields on create', function () {
|
||||
mountChooseUsComponent($this->user)
|
||||
->set('form.name', '')
|
||||
->set('form.description', '')
|
||||
->call('create')
|
||||
->assertHasErrors([
|
||||
'form.name' => 'required',
|
||||
'form.description' => 'required',
|
||||
]);
|
||||
});
|
||||
|
||||
it('validates name max length on create', function () {
|
||||
mountChooseUsComponent($this->user)
|
||||
->set('form.name', str_repeat('a', 51))
|
||||
->set('form.description', 'Valid description')
|
||||
->call('create')
|
||||
->assertHasErrors([
|
||||
'form.name' => 'max',
|
||||
]);
|
||||
});
|
||||
|
||||
it('dispatches refresh datatable event on create', function () {
|
||||
mountChooseUsComponent($this->user)
|
||||
->set('form.name', 'Quality Products')
|
||||
->set('form.description', 'We provide high-quality products.')
|
||||
->call('create')
|
||||
->assertDispatched('refreshDatatable');
|
||||
});
|
||||
|
||||
it('shows success toast on create', function () {
|
||||
// We can't directly test toast messages in Livewire tests
|
||||
// but we can verify the method completes without errors
|
||||
mountChooseUsComponent($this->user)
|
||||
->set('form.name', 'Quality Products')
|
||||
->set('form.description', 'We provide high-quality products.')
|
||||
->call('create');
|
||||
|
||||
expect(ChooseUsModel::count())->toBe(1);
|
||||
});
|
||||
|
||||
it('sets correct sort order on create', function () {
|
||||
// Create first item
|
||||
ChooseUsModel::create([
|
||||
'name' => 'First Item',
|
||||
'description' => 'First description',
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
// Create second item
|
||||
mountChooseUsComponent($this->user)
|
||||
->set('form.name', 'Second Item')
|
||||
->set('form.description', 'Second description')
|
||||
->call('create');
|
||||
|
||||
$secondItem = ChooseUsModel::where('name', 'Second Item')->first();
|
||||
expect($secondItem->sort_order)->toBe(2);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Update Choose Us
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('updates choose us successfully', function () {
|
||||
$chooseUs = ChooseUsModel::factory()->create([
|
||||
'name' => 'Old Name',
|
||||
'description' => 'Old description',
|
||||
]);
|
||||
|
||||
mountChooseUsComponent($this->user)
|
||||
->set('form.chooseUs', $chooseUs)
|
||||
->set('form.name', 'Updated Name')
|
||||
->set('form.description', 'Updated description')
|
||||
->call('update');
|
||||
|
||||
$chooseUs->refresh();
|
||||
expect($chooseUs->name)->toBe('Updated Name');
|
||||
expect($chooseUs->description)->toBe('Updated description');
|
||||
});
|
||||
|
||||
it('validates required fields on update', function () {
|
||||
$chooseUs = ChooseUsModel::factory()->create();
|
||||
|
||||
mountChooseUsComponent($this->user)
|
||||
->set('form.chooseUs', $chooseUs)
|
||||
->set('form.name', '')
|
||||
->set('form.description', '')
|
||||
->call('update')
|
||||
->assertHasErrors([
|
||||
'form.name' => 'required',
|
||||
'form.description' => 'required',
|
||||
]);
|
||||
});
|
||||
|
||||
it('dispatches refresh datatable event on update', function () {
|
||||
$chooseUs = ChooseUsModel::factory()->create();
|
||||
|
||||
mountChooseUsComponent($this->user)
|
||||
->set('form.chooseUs', $chooseUs)
|
||||
->set('form.name', 'Updated Name')
|
||||
->set('form.description', 'Updated description')
|
||||
->call('update')
|
||||
->assertDispatched('refreshDatatable');
|
||||
});
|
||||
|
||||
it('shows success toast on update', function () {
|
||||
$chooseUs = ChooseUsModel::factory()->create([
|
||||
'name' => 'Old Name',
|
||||
'description' => 'Old description',
|
||||
]);
|
||||
|
||||
mountChooseUsComponent($this->user)
|
||||
->set('form.chooseUs', $chooseUs)
|
||||
->set('form.name', 'Updated Name')
|
||||
->set('form.description', 'Updated description')
|
||||
->call('update');
|
||||
|
||||
$chooseUs->refresh();
|
||||
expect($chooseUs->name)->toBe('Updated Name');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Delete Choose Us
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('deletes choose us successfully', function () {
|
||||
$chooseUs = ChooseUsModel::factory()->create();
|
||||
|
||||
mountChooseUsComponent($this->user)
|
||||
->call('delete', $chooseUs);
|
||||
|
||||
expect(ChooseUsModel::find($chooseUs->id))->toBeNull();
|
||||
});
|
||||
|
||||
it('reorders sort order after delete', function () {
|
||||
// Create three items
|
||||
$item1 = ChooseUsModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
|
||||
$item2 = ChooseUsModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
|
||||
$item3 = ChooseUsModel::create(['name' => 'Item 3', 'description' => 'Desc 3', 'sort_order' => 3]);
|
||||
|
||||
// Delete middle item
|
||||
mountChooseUsComponent($this->user)
|
||||
->call('delete', $item2);
|
||||
|
||||
// Refresh remaining items
|
||||
$item1->refresh();
|
||||
$item3->refresh();
|
||||
|
||||
expect($item1->sort_order)->toBe(1);
|
||||
expect($item3->sort_order)->toBe(2);
|
||||
});
|
||||
|
||||
it('dispatches refresh datatable event on delete', function () {
|
||||
$chooseUs = ChooseUsModel::factory()->create();
|
||||
|
||||
mountChooseUsComponent($this->user)
|
||||
->call('delete', $chooseUs)
|
||||
->assertDispatched('refreshDatatable');
|
||||
});
|
||||
|
||||
it('shows success toast on delete', function () {
|
||||
$chooseUs = ChooseUsModel::factory()->create();
|
||||
|
||||
mountChooseUsComponent($this->user)
|
||||
->call('delete', $chooseUs);
|
||||
|
||||
expect(ChooseUsModel::find($chooseUs->id))->toBeNull();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Sorting Functionality
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('moves item up successfully', function () {
|
||||
$item1 = ChooseUsModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
|
||||
$item2 = ChooseUsModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
|
||||
|
||||
mountChooseUsComponent($this->user)
|
||||
->call('sortOrder', $item2, 'up');
|
||||
|
||||
$item1->refresh();
|
||||
$item2->refresh();
|
||||
|
||||
expect($item1->sort_order)->toBe(2);
|
||||
expect($item2->sort_order)->toBe(1);
|
||||
});
|
||||
|
||||
it('moves item down successfully', function () {
|
||||
$item1 = ChooseUsModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
|
||||
$item2 = ChooseUsModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
|
||||
|
||||
mountChooseUsComponent($this->user)
|
||||
->call('sortOrder', $item1, 'down');
|
||||
|
||||
$item1->refresh();
|
||||
$item2->refresh();
|
||||
|
||||
expect($item1->sort_order)->toBe(2);
|
||||
expect($item2->sort_order)->toBe(1);
|
||||
});
|
||||
|
||||
it('moves item to first position', function () {
|
||||
$item1 = ChooseUsModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
|
||||
$item2 = ChooseUsModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
|
||||
$item3 = ChooseUsModel::create(['name' => 'Item 3', 'description' => 'Desc 3', 'sort_order' => 3]);
|
||||
|
||||
mountChooseUsComponent($this->user)
|
||||
->call('sortOrder', $item3, 'first');
|
||||
|
||||
$item1->refresh();
|
||||
$item2->refresh();
|
||||
$item3->refresh();
|
||||
|
||||
expect($item3->sort_order)->toBe(1);
|
||||
expect($item1->sort_order)->toBe(2);
|
||||
expect($item2->sort_order)->toBe(3);
|
||||
});
|
||||
|
||||
it('moves item to last position', function () {
|
||||
$item1 = ChooseUsModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
|
||||
$item2 = ChooseUsModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
|
||||
$item3 = ChooseUsModel::create(['name' => 'Item 3', 'description' => 'Desc 3', 'sort_order' => 3]);
|
||||
|
||||
mountChooseUsComponent($this->user)
|
||||
->call('sortOrder', $item1, 'last');
|
||||
|
||||
$item1->refresh();
|
||||
$item2->refresh();
|
||||
$item3->refresh();
|
||||
|
||||
expect($item1->sort_order)->toBe(3);
|
||||
expect($item2->sort_order)->toBe(1);
|
||||
expect($item3->sort_order)->toBe(2);
|
||||
});
|
||||
|
||||
it('dispatches refresh datatable event on sort', function () {
|
||||
$item1 = ChooseUsModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
|
||||
$item2 = ChooseUsModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
|
||||
|
||||
mountChooseUsComponent($this->user)
|
||||
->call('sortOrder', $item2, 'up')
|
||||
->assertDispatched('refreshDatatable');
|
||||
});
|
||||
|
||||
it('does not move first item up', function () {
|
||||
$item1 = ChooseUsModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
|
||||
$item2 = ChooseUsModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
|
||||
|
||||
mountChooseUsComponent($this->user)
|
||||
->call('sortOrder', $item1, 'up');
|
||||
|
||||
$item1->refresh();
|
||||
$item2->refresh();
|
||||
|
||||
expect($item1->sort_order)->toBe(1);
|
||||
expect($item2->sort_order)->toBe(2);
|
||||
});
|
||||
|
||||
it('does not move last item down', function () {
|
||||
$item1 = ChooseUsModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
|
||||
$item2 = ChooseUsModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
|
||||
|
||||
mountChooseUsComponent($this->user)
|
||||
->call('sortOrder', $item2, 'down');
|
||||
|
||||
$item1->refresh();
|
||||
$item2->refresh();
|
||||
|
||||
expect($item1->sort_order)->toBe(1);
|
||||
expect($item2->sort_order)->toBe(2);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Modal Operations
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('opens modal for create', function () {
|
||||
$component = mountChooseUsComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Pilih Kami');
|
||||
|
||||
expect($component->method)->toBe('create');
|
||||
expect($component->modalTitle)->toBe('Tambah Pilih Kami');
|
||||
});
|
||||
|
||||
it('opens modal for update', function () {
|
||||
$chooseUs = ChooseUsModel::factory()->create([
|
||||
'name' => 'Test Name',
|
||||
'description' => 'Test Description',
|
||||
]);
|
||||
|
||||
$component = mountChooseUsComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Pilih Kami', $chooseUs->hash);
|
||||
|
||||
expect($component->method)->toBe('update');
|
||||
expect($component->modalTitle)->toBe('Edit Pilih Kami');
|
||||
// Note: We can't easily test the form data setting in this context
|
||||
});
|
||||
|
||||
it('resets validation on modal open', function () {
|
||||
// This would require a more complex setup to test validation reset
|
||||
// For now, we'll assume the resetValidation() and resetErrorBag() methods work as expected
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authorization Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('requires create choose us permission', function () {
|
||||
// Create a user without Owner role for this test
|
||||
$userWithoutPermission = User::factory()->active()->create();
|
||||
|
||||
mountChooseUsComponent($userWithoutPermission)
|
||||
->set('form.name', 'Test Name')
|
||||
->set('form.description', 'Test Description')
|
||||
->call('create')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('requires update choose us permission', function () {
|
||||
// Create a user without Owner role for this test
|
||||
$userWithoutPermission = User::factory()->active()->create();
|
||||
|
||||
$chooseUs = ChooseUsModel::factory()->create();
|
||||
|
||||
mountChooseUsComponent($userWithoutPermission)
|
||||
->set('form.chooseUs', $chooseUs)
|
||||
->set('form.name', 'Updated Name')
|
||||
->set('form.description', 'Updated Description')
|
||||
->call('update')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('requires delete choose us permission', function () {
|
||||
// Create a user without Owner role for this test
|
||||
$userWithoutPermission = User::factory()->active()->create();
|
||||
|
||||
$chooseUs = ChooseUsModel::factory()->create();
|
||||
|
||||
mountChooseUsComponent($userWithoutPermission)
|
||||
->call('delete', $chooseUs)
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('requires update choose us permission for sorting', function () {
|
||||
// Create a user without Owner role for this test
|
||||
$userWithoutPermission = User::factory()->active()->create();
|
||||
|
||||
$item1 = ChooseUsModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
|
||||
$item2 = ChooseUsModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
|
||||
|
||||
mountChooseUsComponent($userWithoutPermission)
|
||||
->call('sortOrder', $item2, 'up')
|
||||
->assertForbidden();
|
||||
});
|
||||
@ -1,426 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Studio\Feature\PerfumeFeature;
|
||||
use App\Models\PerfumeFeature as PerfumeFeatureModel;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->create();
|
||||
|
||||
$this->ownerRole = Role::where('name', 'Owner')->first();
|
||||
|
||||
$this->user->roles()->attach($this->ownerRole->id);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders page successfully', function () {
|
||||
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
|
||||
->assertViewIs('livewire.studio.feature.perfume-features')
|
||||
->assertViewHas('pageTitle', 'Keunggulan Parfum');
|
||||
});
|
||||
|
||||
it('mounts with default method', function () {
|
||||
$component = Livewire::actingAs($this->user)->test(PerfumeFeature::class);
|
||||
|
||||
expect($component->method)->toBe('create');
|
||||
expect($component->modalTitle)->toBe('');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Create Perfume Feature
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('creates perfume feature successfully', function () {
|
||||
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
|
||||
->set('form.name', 'Aroma Tahan Lama')
|
||||
->set('form.description', 'Parfum kami memiliki aroma yang tahan lama hingga 24 jam.')
|
||||
->call('create');
|
||||
|
||||
$perfumeFeature = PerfumeFeatureModel::first();
|
||||
expect($perfumeFeature)->not->toBeNull();
|
||||
expect($perfumeFeature->name)->toBe('Aroma Tahan Lama');
|
||||
expect($perfumeFeature->description)->toBe('Parfum kami memiliki aroma yang tahan lama hingga 24 jam.');
|
||||
expect($perfumeFeature->sort_order)->toBe(1);
|
||||
});
|
||||
|
||||
it('validates required fields on create', function () {
|
||||
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
|
||||
->set('form.name', '')
|
||||
->set('form.description', '')
|
||||
->call('create')
|
||||
->assertHasErrors([
|
||||
'form.name' => 'required',
|
||||
'form.description' => 'required',
|
||||
]);
|
||||
});
|
||||
|
||||
it('validates name max length on create', function () {
|
||||
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
|
||||
->set('form.name', str_repeat('a', 51))
|
||||
->set('form.description', 'Valid description')
|
||||
->call('create')
|
||||
->assertHasErrors([
|
||||
'form.name' => 'max',
|
||||
]);
|
||||
});
|
||||
|
||||
it('dispatches refresh datatable event on create', function () {
|
||||
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
|
||||
->set('form.name', 'Aroma Tahan Lama')
|
||||
->set('form.description', 'Parfum kami memiliki aroma yang tahan lama hingga 24 jam.')
|
||||
->call('create')
|
||||
->assertDispatched('refreshDatatable');
|
||||
});
|
||||
|
||||
it('shows success toast on create', function () {
|
||||
// We can't directly test toast messages in Livewire tests
|
||||
// but we can verify the method completes without errors
|
||||
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
|
||||
->set('form.name', 'Aroma Tahan Lama')
|
||||
->set('form.description', 'Parfum kami memiliki aroma yang tahan lama hingga 24 jam.')
|
||||
->call('create');
|
||||
|
||||
expect(PerfumeFeatureModel::count())->toBe(1);
|
||||
});
|
||||
|
||||
it('sets correct sort order on create', function () {
|
||||
// Create first item
|
||||
PerfumeFeatureModel::create([
|
||||
'name' => 'First Item',
|
||||
'description' => 'First description',
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
|
||||
// Create second item
|
||||
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
|
||||
->set('form.name', 'Second Item')
|
||||
->set('form.description', 'Second description')
|
||||
->call('create');
|
||||
|
||||
$secondItem = PerfumeFeatureModel::where('name', 'Second Item')->first();
|
||||
expect($secondItem->sort_order)->toBe(2);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Update Perfume Feature
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('updates perfume feature successfully', function () {
|
||||
$perfumeFeature = PerfumeFeatureModel::factory()->create([
|
||||
'name' => 'Old Name',
|
||||
'description' => 'Old description',
|
||||
]);
|
||||
|
||||
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
|
||||
->set('form.perfumeFeature', $perfumeFeature)
|
||||
->set('form.name', 'Updated Name')
|
||||
->set('form.description', 'Updated description')
|
||||
->call('update');
|
||||
|
||||
$perfumeFeature->refresh();
|
||||
expect($perfumeFeature->name)->toBe('Updated Name');
|
||||
expect($perfumeFeature->description)->toBe('Updated description');
|
||||
});
|
||||
|
||||
it('validates required fields on update', function () {
|
||||
$perfumeFeature = PerfumeFeatureModel::factory()->create();
|
||||
|
||||
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
|
||||
->set('form.perfumeFeature', $perfumeFeature)
|
||||
->set('form.name', '')
|
||||
->set('form.description', '')
|
||||
->call('update')
|
||||
->assertHasErrors([
|
||||
'form.name' => 'required',
|
||||
'form.description' => 'required',
|
||||
]);
|
||||
});
|
||||
|
||||
it('dispatches refresh datatable event on update', function () {
|
||||
$perfumeFeature = PerfumeFeatureModel::factory()->create();
|
||||
|
||||
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
|
||||
->set('form.perfumeFeature', $perfumeFeature)
|
||||
->set('form.name', 'Updated Name')
|
||||
->set('form.description', 'Updated description')
|
||||
->call('update')
|
||||
->assertDispatched('refreshDatatable');
|
||||
});
|
||||
|
||||
it('shows success toast on update', function () {
|
||||
$perfumeFeature = PerfumeFeatureModel::factory()->create([
|
||||
'name' => 'Old Name',
|
||||
'description' => 'Old description',
|
||||
]);
|
||||
|
||||
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
|
||||
->set('form.perfumeFeature', $perfumeFeature)
|
||||
->set('form.name', 'Updated Name')
|
||||
->set('form.description', 'Updated description')
|
||||
->call('update');
|
||||
|
||||
$perfumeFeature->refresh();
|
||||
expect($perfumeFeature->name)->toBe('Updated Name');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Delete Perfume Feature
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('deletes perfume feature successfully', function () {
|
||||
$perfumeFeature = PerfumeFeatureModel::factory()->create();
|
||||
|
||||
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
|
||||
->call('delete', $perfumeFeature);
|
||||
|
||||
expect(PerfumeFeatureModel::find($perfumeFeature->id))->toBeNull();
|
||||
});
|
||||
|
||||
it('reorders sort order after delete', function () {
|
||||
// Create three items
|
||||
$item1 = PerfumeFeatureModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
|
||||
$item2 = PerfumeFeatureModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
|
||||
$item3 = PerfumeFeatureModel::create(['name' => 'Item 3', 'description' => 'Desc 3', 'sort_order' => 3]);
|
||||
|
||||
// Delete middle item
|
||||
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
|
||||
->call('delete', $item2);
|
||||
|
||||
// Refresh remaining items
|
||||
$item1->refresh();
|
||||
$item3->refresh();
|
||||
|
||||
expect($item1->sort_order)->toBe(1);
|
||||
expect($item3->sort_order)->toBe(2);
|
||||
});
|
||||
|
||||
it('dispatches refresh datatable event on delete', function () {
|
||||
$perfumeFeature = PerfumeFeatureModel::factory()->create();
|
||||
|
||||
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
|
||||
->call('delete', $perfumeFeature)
|
||||
->assertDispatched('refreshDatatable');
|
||||
});
|
||||
|
||||
it('shows success toast on delete', function () {
|
||||
$perfumeFeature = PerfumeFeatureModel::factory()->create();
|
||||
|
||||
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
|
||||
->call('delete', $perfumeFeature);
|
||||
|
||||
expect(PerfumeFeatureModel::find($perfumeFeature->id))->toBeNull();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Sorting Functionality
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('moves item up successfully', function () {
|
||||
$item1 = PerfumeFeatureModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
|
||||
$item2 = PerfumeFeatureModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
|
||||
|
||||
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
|
||||
->call('sortOrder', $item2, 'up');
|
||||
|
||||
$item1->refresh();
|
||||
$item2->refresh();
|
||||
|
||||
expect($item1->sort_order)->toBe(2);
|
||||
expect($item2->sort_order)->toBe(1);
|
||||
});
|
||||
|
||||
it('moves item down successfully', function () {
|
||||
$item1 = PerfumeFeatureModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
|
||||
$item2 = PerfumeFeatureModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
|
||||
|
||||
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
|
||||
->call('sortOrder', $item1, 'down');
|
||||
|
||||
$item1->refresh();
|
||||
$item2->refresh();
|
||||
|
||||
expect($item1->sort_order)->toBe(2);
|
||||
expect($item2->sort_order)->toBe(1);
|
||||
});
|
||||
|
||||
it('moves item to first position', function () {
|
||||
$item1 = PerfumeFeatureModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
|
||||
$item2 = PerfumeFeatureModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
|
||||
$item3 = PerfumeFeatureModel::create(['name' => 'Item 3', 'description' => 'Desc 3', 'sort_order' => 3]);
|
||||
|
||||
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
|
||||
->call('sortOrder', $item3, 'first');
|
||||
|
||||
$item1->refresh();
|
||||
$item2->refresh();
|
||||
$item3->refresh();
|
||||
|
||||
expect($item3->sort_order)->toBe(1);
|
||||
expect($item1->sort_order)->toBe(2);
|
||||
expect($item2->sort_order)->toBe(3);
|
||||
});
|
||||
|
||||
it('moves item to last position', function () {
|
||||
$item1 = PerfumeFeatureModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
|
||||
$item2 = PerfumeFeatureModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
|
||||
$item3 = PerfumeFeatureModel::create(['name' => 'Item 3', 'description' => 'Desc 3', 'sort_order' => 3]);
|
||||
|
||||
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
|
||||
->call('sortOrder', $item1, 'last');
|
||||
|
||||
$item1->refresh();
|
||||
$item2->refresh();
|
||||
$item3->refresh();
|
||||
|
||||
expect($item1->sort_order)->toBe(3);
|
||||
expect($item2->sort_order)->toBe(1);
|
||||
expect($item3->sort_order)->toBe(2);
|
||||
});
|
||||
|
||||
it('dispatches refresh datatable event on sort', function () {
|
||||
$item1 = PerfumeFeatureModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
|
||||
$item2 = PerfumeFeatureModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
|
||||
|
||||
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
|
||||
->call('sortOrder', $item2, 'up')
|
||||
->assertDispatched('refreshDatatable');
|
||||
});
|
||||
|
||||
it('does not move first item up', function () {
|
||||
$item1 = PerfumeFeatureModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
|
||||
$item2 = PerfumeFeatureModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
|
||||
|
||||
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
|
||||
->call('sortOrder', $item1, 'up');
|
||||
|
||||
$item1->refresh();
|
||||
$item2->refresh();
|
||||
|
||||
expect($item1->sort_order)->toBe(1);
|
||||
expect($item2->sort_order)->toBe(2);
|
||||
});
|
||||
|
||||
it('does not move last item down', function () {
|
||||
$item1 = PerfumeFeatureModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
|
||||
$item2 = PerfumeFeatureModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
|
||||
|
||||
Livewire::actingAs($this->user)->test(PerfumeFeature::class)
|
||||
->call('sortOrder', $item2, 'down');
|
||||
|
||||
$item1->refresh();
|
||||
$item2->refresh();
|
||||
|
||||
expect($item1->sort_order)->toBe(1);
|
||||
expect($item2->sort_order)->toBe(2);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Modal Operations
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('opens modal for create', function () {
|
||||
$component = Livewire::actingAs($this->user)->test(PerfumeFeature::class)
|
||||
->call('openModal', 'create', 'Tambah Keunggulan Parfum');
|
||||
|
||||
expect($component->method)->toBe('create');
|
||||
expect($component->modalTitle)->toBe('Tambah Keunggulan Parfum');
|
||||
});
|
||||
|
||||
it('opens modal for update', function () {
|
||||
$perfumeFeature = PerfumeFeatureModel::factory()->create([
|
||||
'name' => 'Test Name',
|
||||
'description' => 'Test Description',
|
||||
]);
|
||||
|
||||
$component = Livewire::actingAs($this->user)->test(PerfumeFeature::class)
|
||||
->call('openModal', 'update', 'Edit Keunggulan Parfum', $perfumeFeature->hash);
|
||||
|
||||
expect($component->method)->toBe('update');
|
||||
expect($component->modalTitle)->toBe('Edit Keunggulan Parfum');
|
||||
// Note: We can't easily test the form data setting in this context
|
||||
});
|
||||
|
||||
it('resets validation on modal open', function () {
|
||||
// This would require a more complex setup to test validation reset
|
||||
// For now, we'll assume the resetValidation() and resetErrorBag() methods work as expected
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authorization Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('requires create perfume feature permission', function () {
|
||||
// Create a user without Owner role for this test
|
||||
$userWithoutPermission = User::factory()->active()->create();
|
||||
|
||||
Livewire::actingAs($userWithoutPermission)->test(PerfumeFeature::class)
|
||||
->set('form.name', 'Test Name')
|
||||
->set('form.description', 'Test Description')
|
||||
->call('create')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('requires update perfume feature permission', function () {
|
||||
// Create a user without Owner role for this test
|
||||
$userWithoutPermission = User::factory()->active()->create();
|
||||
|
||||
$perfumeFeature = PerfumeFeatureModel::factory()->create();
|
||||
|
||||
Livewire::actingAs($userWithoutPermission)->test(PerfumeFeature::class)
|
||||
->set('form.perfumeFeature', $perfumeFeature)
|
||||
->set('form.name', 'Updated Name')
|
||||
->set('form.description', 'Updated Description')
|
||||
->call('update')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('requires delete perfume feature permission', function () {
|
||||
// Create a user without Owner role for this test
|
||||
$userWithoutPermission = User::factory()->active()->create();
|
||||
|
||||
$perfumeFeature = PerfumeFeatureModel::factory()->create();
|
||||
|
||||
Livewire::actingAs($userWithoutPermission)->test(PerfumeFeature::class)
|
||||
->call('delete', $perfumeFeature)
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('requires update perfume feature permission for sorting', function () {
|
||||
// Create a user without Owner role for this test
|
||||
$userWithoutPermission = User::factory()->active()->create();
|
||||
|
||||
$item1 = PerfumeFeatureModel::create(['name' => 'Item 1', 'description' => 'Desc 1', 'sort_order' => 1]);
|
||||
$item2 = PerfumeFeatureModel::create(['name' => 'Item 2', 'description' => 'Desc 2', 'sort_order' => 2]);
|
||||
|
||||
Livewire::actingAs($userWithoutPermission)->test(PerfumeFeature::class)
|
||||
->call('sortOrder', $item2, 'up')
|
||||
->assertForbidden();
|
||||
});
|
||||
@ -1,246 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Studio\Finance\Expense;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Expense as ExpenseModel;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
// Setup Permissions
|
||||
Permission::create(['name' => 'create expense']);
|
||||
Permission::create(['name' => 'update expense']);
|
||||
Permission::create(['name' => 'delete expense']);
|
||||
});
|
||||
|
||||
function createUserWithOutlet(bool $multipleOutlets = false)
|
||||
{
|
||||
$user = User::factory()
|
||||
->has(Employee::factory(), 'employee')
|
||||
->create();
|
||||
|
||||
$outlet1 = Outlet::factory()->create();
|
||||
$user->outlets()->attach($outlet1);
|
||||
|
||||
if ($multipleOutlets) {
|
||||
$outlet2 = Outlet::factory()->create();
|
||||
$user->outlets()->attach($outlet2);
|
||||
}
|
||||
|
||||
// Assign permissions
|
||||
$user->givePermissionTo(['create expense', 'update expense', 'delete expense']);
|
||||
|
||||
return [$user, $outlet1];
|
||||
}
|
||||
|
||||
it('renders successfully', function () {
|
||||
[$user, $outlet] = createUserWithOutlet();
|
||||
$this->actingAs($user);
|
||||
|
||||
Livewire::test(Expense::class)
|
||||
->assertStatus(200);
|
||||
});
|
||||
|
||||
it('validates required fields', function () {
|
||||
[$user, $outlet] = createUserWithOutlet();
|
||||
$this->actingAs($user);
|
||||
|
||||
Livewire::test(Expense::class)
|
||||
->set('form.description', '')
|
||||
->set('form.amount', '')
|
||||
->call('create')
|
||||
->assertHasErrors(['form.description', 'form.amount']);
|
||||
});
|
||||
|
||||
it('validates amount must be numeric', function () {
|
||||
[$user, $outlet] = createUserWithOutlet();
|
||||
$this->actingAs($user);
|
||||
|
||||
Livewire::test(Expense::class)
|
||||
->set('form.amount', 'abc')
|
||||
->call('create')
|
||||
->assertHasErrors(['form.amount']);
|
||||
});
|
||||
|
||||
it('validates outlet_id is required for multi-outlet users', function () {
|
||||
[$user, $outlet] = createUserWithOutlet(true); // Multiple outlets
|
||||
$this->actingAs($user);
|
||||
|
||||
Livewire::test(Expense::class)
|
||||
->set('form.description', 'Test Expense')
|
||||
->set('form.amount', '50000')
|
||||
// outlet_id not set
|
||||
->call('create')
|
||||
->assertHasErrors(['form.outlet_id']);
|
||||
});
|
||||
|
||||
it('does not require manually selecting outlet_id for single-outlet users', function () {
|
||||
[$user, $outlet] = createUserWithOutlet(false);
|
||||
$this->actingAs($user);
|
||||
|
||||
Livewire::test(Expense::class)
|
||||
->set('form.description', 'Test Expense')
|
||||
->set('form.amount', '50000')
|
||||
->call('create')
|
||||
->assertHasNoErrors(['form.outlet_id']);
|
||||
});
|
||||
|
||||
it('can create an expense', function () {
|
||||
[$user, $outlet] = createUserWithOutlet();
|
||||
$this->actingAs($user);
|
||||
|
||||
Livewire::test(Expense::class)
|
||||
->set('form.description', 'Buy Coffee')
|
||||
->set('form.amount', '25000')
|
||||
->call('create')
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
$this->assertDatabaseHas('expenses', [
|
||||
'description' => 'Buy Coffee',
|
||||
'amount' => 25000,
|
||||
'user_id' => $user->id,
|
||||
'outlet_id' => $outlet->id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('can create an expense with image', function () {
|
||||
[$user, $outlet] = createUserWithOutlet();
|
||||
$this->actingAs($user);
|
||||
|
||||
$image = UploadedFile::fake()->image('receipt.jpg');
|
||||
// The WithMediaHandler contract seems to expect an array with 'path', not raw file object
|
||||
$imageData = [['path' => $image->getPathname()]];
|
||||
|
||||
Livewire::test(Expense::class)
|
||||
->set('form.description', 'Buy Coffee with Receipt')
|
||||
->set('form.amount', '30000')
|
||||
->set('form.image', $imageData)
|
||||
->call('create');
|
||||
|
||||
$expense = ExpenseModel::where('description', 'Buy Coffee with Receipt')->first();
|
||||
expect($expense)->not->toBeNull();
|
||||
// Assuming media library is set up, verify media count or existence
|
||||
// Note: In-memory/testing sqlite might behave differently with media library if not fully mocked,
|
||||
// but basic relation check is good.
|
||||
expect($expense->getMedia('image'))->toHaveCount(1);
|
||||
});
|
||||
|
||||
it('can update an expense', function () {
|
||||
[$user, $outlet] = createUserWithOutlet();
|
||||
$this->actingAs($user);
|
||||
|
||||
$expense = ExpenseModel::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'outlet_id' => $outlet->id,
|
||||
'description' => 'Old Description',
|
||||
'amount' => 10000,
|
||||
]);
|
||||
|
||||
Livewire::test(Expense::class)
|
||||
->call('openModal', 'update', 'Edit Expense', $expense->hash)
|
||||
->assertSet('form.description', 'Old Description')
|
||||
->set('form.description', 'New Description')
|
||||
->set('form.amount', '15000') // Updating amount
|
||||
->call('update')
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
$this->assertDatabaseHas('expenses', [
|
||||
'id' => $expense->id,
|
||||
'description' => 'New Description',
|
||||
'amount' => 15000,
|
||||
]);
|
||||
});
|
||||
|
||||
it('can update an expense image', function () {
|
||||
[$user, $outlet] = createUserWithOutlet();
|
||||
$this->actingAs($user);
|
||||
|
||||
$expense = ExpenseModel::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'outlet_id' => $outlet->id,
|
||||
'description' => 'Expense with Image',
|
||||
'amount' => 10000,
|
||||
]);
|
||||
|
||||
// Initial image upload (simulated via factory or direct DB check if needed,
|
||||
// but here we just test REPLACING or ADDING via update)
|
||||
|
||||
$newImage = UploadedFile::fake()->image('new_receipt.jpg');
|
||||
$imageData = [['path' => $newImage->getPathname()]];
|
||||
|
||||
Livewire::test(Expense::class)
|
||||
->call('openModal', 'update', 'Edit Expense', $expense->hash)
|
||||
->set('form.image', $imageData)
|
||||
->call('update');
|
||||
|
||||
$expense->refresh();
|
||||
expect($expense->getMedia('image'))->toHaveCount(1);
|
||||
// In a real scenario we'd check filename, but hash names make it tricky in tests without mocking 'fake'
|
||||
});
|
||||
|
||||
it('can delete an expense', function () {
|
||||
[$user, $outlet] = createUserWithOutlet();
|
||||
$this->actingAs($user);
|
||||
|
||||
$expense = ExpenseModel::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'outlet_id' => $outlet->id,
|
||||
]);
|
||||
|
||||
Livewire::test(Expense::class)
|
||||
->call('delete', $expense->hash)
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
$this->assertSoftDeleted('expenses', [
|
||||
'id' => $expense->id,
|
||||
]);
|
||||
});
|
||||
|
||||
// AUTHORIZATION TESTS
|
||||
|
||||
it('checks authorization for create', function () {
|
||||
[$user, $outlet] = createUserWithOutlet();
|
||||
$user->revokePermissionTo('create expense');
|
||||
$this->actingAs($user);
|
||||
|
||||
Livewire::test(Expense::class)
|
||||
->call('create')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('checks authorization for update', function () {
|
||||
[$user, $outlet] = createUserWithOutlet();
|
||||
$user->revokePermissionTo('update expense');
|
||||
$this->actingAs($user);
|
||||
|
||||
$expense = ExpenseModel::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'outlet_id' => $outlet->id,
|
||||
]);
|
||||
|
||||
Livewire::test(Expense::class)
|
||||
->call('openModal', 'update', 'Edit', $expense->hash) // openModal doesn't check perm, update() does
|
||||
->call('update')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('checks authorization for delete', function () {
|
||||
[$user, $outlet] = createUserWithOutlet();
|
||||
$user->revokePermissionTo('delete expense');
|
||||
$this->actingAs($user);
|
||||
|
||||
$expense = ExpenseModel::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'outlet_id' => $outlet->id,
|
||||
]);
|
||||
|
||||
Livewire::test(Expense::class)
|
||||
->call('delete', $expense->hash)
|
||||
->assertForbidden();
|
||||
});
|
||||
@ -1,194 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Jobs\StorePushNotification;
|
||||
use App\Livewire\Studio\Information\Broadcast;
|
||||
use App\Models\Broadcast as BroadcastModel;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
// Create permissions
|
||||
Permission::create(['name' => 'create broadcast', 'guard_name' => 'web']);
|
||||
Permission::create(['name' => 'update broadcast', 'guard_name' => 'web']);
|
||||
Permission::create(['name' => 'delete broadcast', 'guard_name' => 'web']);
|
||||
|
||||
// Create role and assign permissions
|
||||
$role = Role::create(['name' => 'Admin', 'guard_name' => 'web']);
|
||||
$role->givePermissionTo(['create broadcast', 'update broadcast', 'delete broadcast']);
|
||||
|
||||
// Create admin user
|
||||
$this->adminUser = User::factory()->create();
|
||||
$this->adminUser->assignRole('Admin');
|
||||
|
||||
// Create additional roles for testing
|
||||
Role::create(['name' => 'User', 'guard_name' => 'web']);
|
||||
Role::create(['name' => 'Manager', 'guard_name' => 'web']);
|
||||
});
|
||||
|
||||
it('renders page successfully', function () {
|
||||
Livewire::actingAs($this->adminUser)
|
||||
->test(Broadcast::class)
|
||||
->assertOk()
|
||||
->assertSet('method', 'create')
|
||||
->assertSet('modalTitle', '')
|
||||
->assertSee('Broadcast')
|
||||
->assertSet('roles', function ($roles) {
|
||||
return count($roles) > 0 && ! isset($roles['Developer']);
|
||||
});
|
||||
});
|
||||
|
||||
it('has title attribute', function () {
|
||||
$component = new Broadcast;
|
||||
$reflection = new \ReflectionClass($component);
|
||||
$attributes = $reflection->getAttributes(\Livewire\Attributes\Title::class);
|
||||
|
||||
expect($attributes)->not->toBeEmpty();
|
||||
});
|
||||
|
||||
it('excludes developer role on mount', function () {
|
||||
Role::create(['name' => 'Developer', 'guard_name' => 'web']);
|
||||
|
||||
Livewire::actingAs($this->adminUser)
|
||||
->test(Broadcast::class)
|
||||
->assertSet('roles', function ($roles) {
|
||||
return ! isset($roles['Developer']);
|
||||
});
|
||||
});
|
||||
|
||||
it('opens modal for create', function () {
|
||||
Livewire::actingAs($this->adminUser)
|
||||
->test(Broadcast::class)
|
||||
->call('openModal', 'create', 'Tambah Broadcast')
|
||||
->assertSet('method', 'create')
|
||||
->assertSet('modalTitle', 'Tambah Broadcast')
|
||||
->assertSet('form.title', '')
|
||||
->assertSet('form.body', '')
|
||||
->assertSet('form.role_ids', []);
|
||||
});
|
||||
|
||||
it('opens modal for update', function () {
|
||||
$broadcast = BroadcastModel::factory()->create([
|
||||
'title' => 'Test Title',
|
||||
'body' => 'Test Body',
|
||||
]);
|
||||
|
||||
$role = Role::create(['name' => 'Test Role', 'guard_name' => 'web']);
|
||||
$broadcast->audiences()->attach($role->id);
|
||||
|
||||
Livewire::actingAs($this->adminUser)
|
||||
->test(Broadcast::class)
|
||||
->call('openModal', 'update', 'Ubah Broadcast', $broadcast->hash)
|
||||
->assertSet('method', 'update')
|
||||
->assertSet('modalTitle', 'Ubah Broadcast');
|
||||
});
|
||||
|
||||
it('creates broadcast successfully', function () {
|
||||
Queue::fake();
|
||||
|
||||
$role1 = Role::where('name', 'User')->first();
|
||||
$role2 = Role::where('name', 'Manager')->first();
|
||||
|
||||
$user1 = User::factory()->create();
|
||||
$user2 = User::factory()->create();
|
||||
$user1->assignRole('User');
|
||||
$user2->assignRole('Manager');
|
||||
|
||||
Livewire::actingAs($this->adminUser)
|
||||
->test(Broadcast::class)
|
||||
->set('form.title', 'Important Announcement')
|
||||
->set('form.body', 'This is a test broadcast message.')
|
||||
->set('form.role_ids', [$role1->id, $role2->id])
|
||||
->call('create')
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
$this->assertDatabaseHas('broadcasts', [
|
||||
'user_id' => $this->adminUser->id,
|
||||
'title' => 'Important Announcement',
|
||||
'body' => 'This is a test broadcast message.',
|
||||
]);
|
||||
|
||||
$broadcast = BroadcastModel::latest()->first();
|
||||
expect($broadcast->audiences)->toHaveCount(2);
|
||||
|
||||
Queue::assertPushed(StorePushNotification::class);
|
||||
});
|
||||
|
||||
it('requires permission to create broadcast', function () {
|
||||
$userWithoutPermission = User::factory()->create();
|
||||
|
||||
Livewire::actingAs($userWithoutPermission)
|
||||
->test(Broadcast::class)
|
||||
->set('form.title', 'Test Title')
|
||||
->set('form.body', 'Test Body')
|
||||
->set('form.role_ids', [1])
|
||||
->call('create')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('broadcasts cannot be updated', function () {
|
||||
$broadcast = BroadcastModel::factory()->create();
|
||||
|
||||
// Broadcast component doesn't have update method, only create and delete
|
||||
expect(method_exists(Broadcast::class, 'update'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('broadcasts do not support update permission check', function () {
|
||||
// Broadcast component doesn't have update method, so no update permission test needed
|
||||
expect(method_exists(Broadcast::class, 'update'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('deletes broadcast successfully', function () {
|
||||
$broadcast = BroadcastModel::factory()->create();
|
||||
|
||||
Livewire::actingAs($this->adminUser)
|
||||
->test(Broadcast::class)
|
||||
->call('delete', $broadcast)
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
$this->assertDatabaseMissing('broadcasts', [
|
||||
'id' => $broadcast->id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('displays validation errors', function () {
|
||||
Livewire::actingAs($this->adminUser)
|
||||
->test(Broadcast::class)
|
||||
->set('form.title', '') // Empty title
|
||||
->set('form.body', '') // Empty body
|
||||
->set('form.role_ids', []) // Empty role_ids
|
||||
->call('create')
|
||||
->assertHasErrors(['form.title', 'form.body', 'form.role_ids']);
|
||||
});
|
||||
|
||||
it('opens modal correctly', function () {
|
||||
$broadcast = BroadcastModel::factory()->create();
|
||||
|
||||
Livewire::actingAs($this->adminUser)
|
||||
->test(Broadcast::class)
|
||||
->call('openModal', 'create', 'Tambah Broadcast')
|
||||
->assertSet('method', 'create')
|
||||
->assertSet('modalTitle', 'Tambah Broadcast');
|
||||
});
|
||||
|
||||
it('creates broadcast with push notification job', function () {
|
||||
Queue::fake();
|
||||
|
||||
$role = Role::where('name', 'User')->first();
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole('User');
|
||||
|
||||
Livewire::actingAs($this->adminUser)
|
||||
->test(Broadcast::class)
|
||||
->set('form.title', 'Test Title')
|
||||
->set('form.body', 'Test Body')
|
||||
->set('form.role_ids', [$role->id])
|
||||
->call('create');
|
||||
|
||||
Queue::assertPushed(StorePushNotification::class);
|
||||
});
|
||||
@ -1,194 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Studio\Information\Faq;
|
||||
use App\Models\Faq as FaqModel;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
// Create permissions
|
||||
Permission::create(['name' => 'create faq', 'guard_name' => 'web']);
|
||||
Permission::create(['name' => 'update faq', 'guard_name' => 'web']);
|
||||
Permission::create(['name' => 'delete faq', 'guard_name' => 'web']);
|
||||
|
||||
// Create role and assign permissions
|
||||
$role = Role::create(['name' => 'Admin', 'guard_name' => 'web']);
|
||||
$role->givePermissionTo(['create faq', 'update faq', 'delete faq']);
|
||||
|
||||
// Create admin user
|
||||
$this->adminUser = User::factory()->create();
|
||||
$this->adminUser->assignRole('Admin');
|
||||
});
|
||||
|
||||
it('renders page successfully', function () {
|
||||
Livewire::actingAs($this->adminUser)
|
||||
->test(Faq::class)
|
||||
->assertOk()
|
||||
->assertSet('method', 'create')
|
||||
->assertSet('modalTitle', '')
|
||||
->assertSee('FAQs');
|
||||
});
|
||||
|
||||
it('has title attribute', function () {
|
||||
$component = new Faq;
|
||||
$reflection = new \ReflectionClass($component);
|
||||
$attributes = $reflection->getAttributes(\Livewire\Attributes\Title::class);
|
||||
|
||||
expect($attributes)->not->toBeEmpty();
|
||||
});
|
||||
|
||||
it('opens modal for create', function () {
|
||||
Livewire::actingAs($this->adminUser)
|
||||
->test(Faq::class)
|
||||
->call('openModal', 'create', 'Tambah FAQs')
|
||||
->assertSet('method', 'create')
|
||||
->assertSet('modalTitle', 'Tambah FAQs')
|
||||
->assertSet('form.question', '')
|
||||
->assertSet('form.answer', '');
|
||||
});
|
||||
|
||||
it('opens modal for update', function () {
|
||||
$faq = FaqModel::factory()->create([
|
||||
'question' => 'Test Question',
|
||||
'answer' => 'Test Answer',
|
||||
]);
|
||||
|
||||
Livewire::actingAs($this->adminUser)
|
||||
->test(Faq::class)
|
||||
->call('openModal', 'update', 'Ubah FAQs', $faq->hash)
|
||||
->assertSet('method', 'update')
|
||||
->assertSet('modalTitle', 'Ubah FAQs')
|
||||
->assertSet('form.question', 'Test Question')
|
||||
->assertSet('form.answer', 'Test Answer');
|
||||
});
|
||||
|
||||
it('creates faq successfully', function () {
|
||||
Livewire::actingAs($this->adminUser)
|
||||
->test(Faq::class)
|
||||
->set('form.question', 'What is Laravel?')
|
||||
->set('form.answer', 'Laravel is a PHP framework.')
|
||||
->call('create')
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
$this->assertDatabaseHas('faqs', [
|
||||
'question' => 'What is Laravel?',
|
||||
'answer' => 'Laravel is a PHP framework.',
|
||||
'sort_order' => 1,
|
||||
]);
|
||||
});
|
||||
|
||||
it('requires permission to create faq', function () {
|
||||
$userWithoutPermission = User::factory()->create();
|
||||
|
||||
Livewire::actingAs($userWithoutPermission)
|
||||
->test(Faq::class)
|
||||
->set('form.question', 'Test Question')
|
||||
->set('form.answer', 'Test Answer')
|
||||
->call('create')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('updates faq successfully', function () {
|
||||
$faq = FaqModel::factory()->create([
|
||||
'question' => 'Old Question',
|
||||
'answer' => 'Old Answer',
|
||||
]);
|
||||
|
||||
Livewire::actingAs($this->adminUser)
|
||||
->test(Faq::class)
|
||||
->call('openModal', 'update', 'Ubah FAQs', $faq->hash)
|
||||
->set('form.question', 'Updated Question')
|
||||
->set('form.answer', 'Updated Answer')
|
||||
->call('update')
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
$this->assertDatabaseHas('faqs', [
|
||||
'id' => $faq->id,
|
||||
'question' => 'Updated Question',
|
||||
'answer' => 'Updated Answer',
|
||||
]);
|
||||
});
|
||||
|
||||
it('requires permission to update faq', function () {
|
||||
$faq = FaqModel::factory()->create();
|
||||
$userWithoutPermission = User::factory()->create();
|
||||
|
||||
Livewire::actingAs($userWithoutPermission)
|
||||
->test(Faq::class)
|
||||
->call('openModal', 'update', 'Ubah FAQs', $faq->hash)
|
||||
->set('form.question', 'Updated Question')
|
||||
->set('form.answer', 'Updated Answer')
|
||||
->call('update')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('deletes faq successfully', function () {
|
||||
$faq = FaqModel::factory()->create();
|
||||
|
||||
Livewire::actingAs($this->adminUser)
|
||||
->test(Faq::class)
|
||||
->call('delete', $faq)
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
expect($faq->trashed())->toBeTrue();
|
||||
});
|
||||
|
||||
it('sorts order up', function () {
|
||||
$faq1 = FaqModel::factory()->create(['sort_order' => 1]);
|
||||
$faq2 = FaqModel::factory()->create(['sort_order' => 2]);
|
||||
|
||||
Livewire::actingAs($this->adminUser)
|
||||
->test(Faq::class)
|
||||
->call('sortOrder', $faq2, 'up')
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
expect($faq1->fresh()->sort_order)->toBe(2);
|
||||
expect($faq2->fresh()->sort_order)->toBe(1);
|
||||
});
|
||||
|
||||
it('sorts order down', function () {
|
||||
$faq1 = FaqModel::factory()->create(['sort_order' => 1]);
|
||||
$faq2 = FaqModel::factory()->create(['sort_order' => 2]);
|
||||
|
||||
Livewire::actingAs($this->adminUser)
|
||||
->test(Faq::class)
|
||||
->call('sortOrder', $faq1, 'down')
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
expect($faq1->fresh()->sort_order)->toBe(2);
|
||||
expect($faq2->fresh()->sort_order)->toBe(1);
|
||||
});
|
||||
|
||||
it('requires permission to sort order', function () {
|
||||
$faq = FaqModel::factory()->create();
|
||||
$userWithoutPermission = User::factory()->create();
|
||||
|
||||
Livewire::actingAs($userWithoutPermission)
|
||||
->test(Faq::class)
|
||||
->call('sortOrder', $faq, 'up')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('displays validation errors', function () {
|
||||
Livewire::actingAs($this->adminUser)
|
||||
->test(Faq::class)
|
||||
->set('form.question', '') // Empty question
|
||||
->set('form.answer', '') // Empty answer
|
||||
->call('create')
|
||||
->assertHasErrors(['form.question', 'form.answer']);
|
||||
});
|
||||
|
||||
it('opens modal correctly', function () {
|
||||
$faq = FaqModel::factory()->create();
|
||||
|
||||
Livewire::actingAs($this->adminUser)
|
||||
->test(Faq::class)
|
||||
->call('openModal', 'create', 'Tambah FAQs')
|
||||
->assertSet('method', 'create')
|
||||
->assertSet('modalTitle', 'Tambah FAQs');
|
||||
});
|
||||
@ -1,643 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\Gender;
|
||||
use App\Livewire\Studio\Loyalty\Customer as CustomerComponent;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Order;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
|
||||
$this->ownerRole = Role::where('name', 'Owner')->first();
|
||||
|
||||
$this->user->roles()->attach($this->ownerRole->id);
|
||||
});
|
||||
|
||||
function mountCustomerComponent(User $user)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(CustomerComponent::class);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders page successfully', function () {
|
||||
mountCustomerComponent($this->user)
|
||||
->assertViewIs('livewire.studio.loyalty.customers')
|
||||
->assertViewHas('pageTitle', 'Customer');
|
||||
});
|
||||
|
||||
it('initializes form properties on mount', function () {
|
||||
$component = mountCustomerComponent($this->user);
|
||||
|
||||
expect($component->method)->toBe('create');
|
||||
expect($component->modalTitle)->toBe('');
|
||||
expect($component->form->name)->toBe('');
|
||||
expect($component->form->phone_number)->toBeNull();
|
||||
expect($component->form->gender)->toBe('');
|
||||
});
|
||||
|
||||
it('loads stats on mount', function () {
|
||||
// Create customers without users
|
||||
Customer::factory()->count(3)->create(['user_id' => null]);
|
||||
|
||||
// Create customers with users
|
||||
$user1 = User::factory()->create();
|
||||
$user2 = User::factory()->create();
|
||||
Customer::factory()->create(['user_id' => $user1->id]);
|
||||
Customer::factory()->create(['user_id' => $user2->id]);
|
||||
|
||||
$component = mountCustomerComponent($this->user);
|
||||
|
||||
expect($component->stats)->toBeArray();
|
||||
expect($component->stats)->toHaveCount(3);
|
||||
expect($component->stats[0]['title'])->toBe('Total Customer');
|
||||
expect($component->stats[1]['title'])->toBe('Total Member');
|
||||
expect($component->stats[2]['title'])->toBe('Top Member');
|
||||
});
|
||||
|
||||
it('calculates total customer count correctly', function () {
|
||||
Customer::factory()->count(5)->create(['user_id' => null]);
|
||||
Customer::factory()->count(3)->create(['user_id' => User::factory()->create()->id]);
|
||||
|
||||
$component = mountCustomerComponent($this->user);
|
||||
|
||||
expect($component->stats[0]['value'])->toBe(5);
|
||||
});
|
||||
|
||||
it('calculates total member count correctly', function () {
|
||||
Customer::factory()->count(2)->create(['user_id' => null]);
|
||||
$user1 = User::factory()->create();
|
||||
$user2 = User::factory()->create();
|
||||
Customer::factory()->create(['user_id' => $user1->id]);
|
||||
Customer::factory()->create(['user_id' => $user2->id]);
|
||||
|
||||
$component = mountCustomerComponent($this->user);
|
||||
|
||||
expect($component->stats[1]['value'])->toBe(2);
|
||||
});
|
||||
|
||||
it('displays top member name correctly', function () {
|
||||
$user1 = User::factory()->create();
|
||||
$user2 = User::factory()->create();
|
||||
$customer1 = Customer::factory()->create(['user_id' => $user1->id, 'name' => 'Top Customer']);
|
||||
$customer2 = Customer::factory()->create(['user_id' => $user2->id, 'name' => 'Regular Customer']);
|
||||
|
||||
// Create more orders for customer1
|
||||
$outlet = Outlet::factory()->create();
|
||||
Order::factory()->count(5)->create(['customer_id' => $customer1->id, 'user_id' => $user1->id, 'outlet_id' => $outlet->id]);
|
||||
Order::factory()->count(2)->create(['customer_id' => $customer2->id, 'user_id' => $user2->id, 'outlet_id' => $outlet->id]);
|
||||
|
||||
$component = mountCustomerComponent($this->user);
|
||||
|
||||
expect($component->stats[2]['value'])->toBe('Top Customer');
|
||||
});
|
||||
|
||||
it('displays null when no members exist', function () {
|
||||
Customer::factory()->count(3)->create(['user_id' => null]);
|
||||
|
||||
$component = mountCustomerComponent($this->user);
|
||||
|
||||
expect($component->stats[2]['value'])->toBeNull();
|
||||
});
|
||||
|
||||
it('displays null when members have no orders', function () {
|
||||
$user = User::factory()->create();
|
||||
Customer::factory()->create(['user_id' => $user->id]);
|
||||
|
||||
$component = mountCustomerComponent($this->user);
|
||||
|
||||
expect($component->stats[2]['value'])->toBeNull();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Modal Management
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('opens modal with create method', function () {
|
||||
$component = mountCustomerComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Customer');
|
||||
|
||||
expect($component->method)->toBe('create');
|
||||
expect($component->modalTitle)->toBe('Tambah Customer');
|
||||
});
|
||||
|
||||
it('opens modal with update method', function () {
|
||||
$customer = Customer::factory()->create();
|
||||
|
||||
$component = mountCustomerComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Customer', $customer->hash);
|
||||
|
||||
expect($component->method)->toBe('update');
|
||||
expect($component->modalTitle)->toBe('Edit Customer');
|
||||
});
|
||||
|
||||
it('sets form data when opening update modal', function () {
|
||||
$customer = Customer::factory()->create([
|
||||
'name' => 'John Doe',
|
||||
'phone_number' => '0812 3456 7890',
|
||||
'gender' => Gender::MALE->value,
|
||||
]);
|
||||
|
||||
$component = mountCustomerComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Customer', $customer->hash);
|
||||
|
||||
expect($component->form->name)->toBe('John Doe');
|
||||
expect($component->form->phone_number)->toBe('0812 3456 7890');
|
||||
expect($component->form->gender)->toEqual(Gender::MALE->value);
|
||||
});
|
||||
|
||||
it('resets validation and error bag when opening modal', function () {
|
||||
$component = mountCustomerComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Customer')
|
||||
->set('form.name', '')
|
||||
->call('create')
|
||||
->assertHasErrors(['form.name']);
|
||||
|
||||
$component->call('openModal', 'create', 'Tambah Customer')
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Create Customer
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('prevents create when user is unauthorized', function () {
|
||||
$this->ownerRole->revokePermissionTo('create customer');
|
||||
|
||||
Gate::define('create customer', fn () => false);
|
||||
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Customer')
|
||||
->set('form.name', 'John Doe')
|
||||
->set('form.gender', Gender::MALE->value)
|
||||
->call('create')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('can create a customer when authorized', function () {
|
||||
$data = [
|
||||
'name' => 'John Doe',
|
||||
'phone_number' => '0812 3456 7890',
|
||||
'gender' => Gender::MALE->value,
|
||||
];
|
||||
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Customer')
|
||||
->set('form.name', $data['name'])
|
||||
->set('form.phone_number', $data['phone_number'])
|
||||
->set('form.gender', $data['gender'])
|
||||
->call('create')
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
$this->assertDatabaseHas('customers', [
|
||||
'name' => $data['name'],
|
||||
'phone_number' => $data['phone_number'],
|
||||
'gender' => Gender::MALE->value,
|
||||
]);
|
||||
});
|
||||
|
||||
it('can create a customer without phone number', function () {
|
||||
$data = [
|
||||
'name' => 'Jane Doe',
|
||||
'phone_number' => null,
|
||||
'gender' => Gender::FEMALE->value,
|
||||
];
|
||||
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Customer')
|
||||
->set('form.name', $data['name'])
|
||||
->set('form.phone_number', $data['phone_number'])
|
||||
->set('form.gender', $data['gender'])
|
||||
->call('create')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$this->assertDatabaseHas('customers', [
|
||||
'name' => $data['name'],
|
||||
'phone_number' => null,
|
||||
'gender' => Gender::FEMALE->value,
|
||||
]);
|
||||
});
|
||||
|
||||
it('can create a customer with user_id', function () {
|
||||
$user = User::factory()->create();
|
||||
$data = [
|
||||
'name' => 'Member Customer',
|
||||
'gender' => Gender::MALE->value,
|
||||
];
|
||||
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Customer')
|
||||
->set('form.name', $data['name'])
|
||||
->set('form.gender', $data['gender'])
|
||||
->set('form.user_id', $user->id)
|
||||
->call('create')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$this->assertDatabaseHas('customers', [
|
||||
'name' => $data['name'],
|
||||
'user_id' => $user->id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('validates required fields when creating customer', function () {
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Customer')
|
||||
->call('create')
|
||||
->assertHasErrors([
|
||||
'form.name',
|
||||
'form.gender',
|
||||
]);
|
||||
});
|
||||
|
||||
it('validates name field max length when creating customer', function () {
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Customer')
|
||||
->set('form.name', str_repeat('a', 51))
|
||||
->set('form.gender', Gender::MALE->value)
|
||||
->call('create')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates gender field is required when creating customer', function () {
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Customer')
|
||||
->set('form.name', 'John Doe')
|
||||
->set('form.gender', '')
|
||||
->call('create')
|
||||
->assertHasErrors(['form.gender']);
|
||||
});
|
||||
|
||||
it('validates gender field is valid enum when creating customer', function () {
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Customer')
|
||||
->set('form.name', 'John Doe')
|
||||
->set('form.gender', 'invalid')
|
||||
->call('create')
|
||||
->assertHasErrors(['form.gender']);
|
||||
});
|
||||
|
||||
it('validates phone number format when creating customer', function () {
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Customer')
|
||||
->set('form.name', 'John Doe')
|
||||
->set('form.phone_number', 'invalid-phone')
|
||||
->set('form.gender', Gender::MALE->value)
|
||||
->call('create')
|
||||
->assertHasErrors(['form.phone_number']);
|
||||
});
|
||||
|
||||
it('accepts valid phone number format when creating customer', function () {
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Customer')
|
||||
->set('form.name', 'John Doe')
|
||||
->set('form.phone_number', '0812 3456 7890')
|
||||
->set('form.gender', Gender::MALE->value)
|
||||
->call('create')
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
|
||||
it('dispatches refreshDatatable event after creating customer', function () {
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Customer')
|
||||
->set('form.name', 'John Doe')
|
||||
->set('form.gender', Gender::MALE->value)
|
||||
->call('create')
|
||||
->assertDispatched('refreshDatatable');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Update Customer
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('prevents update when user is unauthorized', function () {
|
||||
$this->ownerRole->revokePermissionTo('update customer');
|
||||
Gate::define('update customer', fn () => false);
|
||||
|
||||
$customer = Customer::factory()->create();
|
||||
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Customer', $customer->hash)
|
||||
->set('form.name', 'Updated Name')
|
||||
->call('update')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('can update a customer when authorized', function () {
|
||||
$customer = Customer::factory()->create([
|
||||
'name' => 'John Doe',
|
||||
'phone_number' => '0812 3456 7890',
|
||||
'gender' => Gender::MALE,
|
||||
]);
|
||||
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Customer', $customer->hash)
|
||||
->assertSet('form.name', 'John Doe')
|
||||
->assertSet('form.phone_number', '0812 3456 7890')
|
||||
->assertSet('form.gender', Gender::MALE->value)
|
||||
->set('form.name', 'Jane Doe')
|
||||
->set('form.phone_number', '0812 9999 8888')
|
||||
->set('form.gender', Gender::FEMALE->value)
|
||||
->call('update')
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
$customer->refresh();
|
||||
expect($customer->name)->toBe('Jane Doe');
|
||||
expect($customer->phone_number)->toBe('0812 9999 8888');
|
||||
expect($customer->gender)->toBe(Gender::FEMALE);
|
||||
});
|
||||
|
||||
it('can update customer phone number to null', function () {
|
||||
$customer = Customer::factory()->create([
|
||||
'phone_number' => '0812 3456 7890',
|
||||
]);
|
||||
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Customer', $customer->hash)
|
||||
->set('form.phone_number', null)
|
||||
->call('update')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$customer->refresh();
|
||||
expect($customer->phone_number)->toBeNull();
|
||||
});
|
||||
|
||||
it('validates required fields when updating customer', function () {
|
||||
$customer = Customer::factory()->create();
|
||||
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Customer', $customer->hash)
|
||||
->set('form.name', '')
|
||||
->set('form.gender', '')
|
||||
->call('update')
|
||||
->assertHasErrors([
|
||||
'form.name',
|
||||
'form.gender',
|
||||
]);
|
||||
});
|
||||
|
||||
it('validates name field max length when updating customer', function () {
|
||||
$customer = Customer::factory()->create();
|
||||
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Customer', $customer->hash)
|
||||
->set('form.name', str_repeat('a', 51))
|
||||
->call('update')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates phone number format when updating customer', function () {
|
||||
$customer = Customer::factory()->create();
|
||||
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Customer', $customer->hash)
|
||||
->set('form.phone_number', 'invalid-phone')
|
||||
->call('update')
|
||||
->assertHasErrors(['form.phone_number']);
|
||||
});
|
||||
|
||||
it('dispatches refreshDatatable event after updating customer', function () {
|
||||
$customer = Customer::factory()->create();
|
||||
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Customer', $customer->hash)
|
||||
->set('form.name', 'Updated Name')
|
||||
->call('update')
|
||||
->assertDispatched('refreshDatatable');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Delete Customer
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('can delete a customer', function () {
|
||||
$customer = Customer::factory()->create();
|
||||
|
||||
mountCustomerComponent($this->user)
|
||||
->call('delete', $customer)
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
expect(Customer::count())->toBe(0);
|
||||
$this->assertSoftDeleted('customers', ['id' => $customer->id]);
|
||||
});
|
||||
|
||||
it('soft deletes customer when deleting', function () {
|
||||
$customer = Customer::factory()->create();
|
||||
|
||||
mountCustomerComponent($this->user)
|
||||
->call('delete', $customer);
|
||||
|
||||
$customer->refresh();
|
||||
expect($customer->trashed())->toBeTrue();
|
||||
});
|
||||
|
||||
it('dispatches refreshDatatable event after deleting customer', function () {
|
||||
$customer = Customer::factory()->create();
|
||||
|
||||
mountCustomerComponent($this->user)
|
||||
->call('delete', $customer)
|
||||
->assertDispatched('refreshDatatable');
|
||||
});
|
||||
|
||||
it('can delete customer with orders', function () {
|
||||
$outlet = Outlet::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$customer = Customer::factory()->create(['user_id' => $user->id]);
|
||||
Order::factory()->count(3)->create([
|
||||
'customer_id' => $customer->id,
|
||||
'user_id' => $user->id,
|
||||
'outlet_id' => $outlet->id,
|
||||
]);
|
||||
|
||||
mountCustomerComponent($this->user)
|
||||
->call('delete', $customer);
|
||||
|
||||
expect($customer->trashed())->toBeTrue();
|
||||
// Orders should still exist (cascade delete might be configured differently)
|
||||
expect(Order::count())->toBe(3);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Stats Recalculation
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('recalculates stats after creating customer', function () {
|
||||
$initialStats = mountCustomerComponent($this->user)->stats;
|
||||
$initialCustomerCount = $initialStats[0]['value'];
|
||||
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Customer')
|
||||
->set('form.name', 'New Customer')
|
||||
->set('form.gender', Gender::MALE->value)
|
||||
->call('create');
|
||||
|
||||
$newComponent = mountCustomerComponent($this->user);
|
||||
expect($newComponent->stats[0]['value'])->toBe($initialCustomerCount + 1);
|
||||
});
|
||||
|
||||
it('recalculates stats after creating member customer', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Customer')
|
||||
->set('form.name', 'New Member')
|
||||
->set('form.gender', Gender::MALE->value)
|
||||
->set('form.user_id', $user->id)
|
||||
->call('create');
|
||||
|
||||
$newComponent = mountCustomerComponent($this->user);
|
||||
expect($newComponent->stats[1]['value'])->toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('recalculates top member after creating orders', function () {
|
||||
$user = User::factory()->create();
|
||||
$customer = Customer::factory()->create(['user_id' => $user->id, 'name' => 'Top Member']);
|
||||
$outlet = Outlet::factory()->create();
|
||||
|
||||
Order::factory()->count(5)->create([
|
||||
'customer_id' => $customer->id,
|
||||
'user_id' => $user->id,
|
||||
'outlet_id' => $outlet->id,
|
||||
]);
|
||||
|
||||
$component = mountCustomerComponent($this->user);
|
||||
expect($component->stats[2]['value'])->toBe('Top Member');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Edge Cases
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('handles customer with very long name at max length', function () {
|
||||
$longName = str_repeat('a', 50);
|
||||
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Customer')
|
||||
->set('form.name', $longName)
|
||||
->set('form.gender', Gender::MALE->value)
|
||||
->call('create')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$this->assertDatabaseHas('customers', ['name' => $longName]);
|
||||
});
|
||||
|
||||
it('handles customer with all gender types', function () {
|
||||
foreach (Gender::cases() as $gender) {
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Customer')
|
||||
->set('form.name', "Customer {$gender->name}")
|
||||
->set('form.gender', $gender->value)
|
||||
->call('create')
|
||||
->assertHasNoErrors();
|
||||
}
|
||||
|
||||
expect(Customer::where('gender', Gender::MALE->value)->count())->toBeGreaterThan(0);
|
||||
expect(Customer::where('gender', Gender::FEMALE->value)->count())->toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('handles updating customer with same data', function () {
|
||||
$customer = Customer::factory()->create([
|
||||
'name' => 'John Doe',
|
||||
'phone_number' => '0812 3456 7890',
|
||||
'gender' => Gender::MALE,
|
||||
]);
|
||||
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'update', 'Edit Customer', $customer->hash)
|
||||
->call('update')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$customer->refresh();
|
||||
expect($customer->name)->toBe('John Doe');
|
||||
});
|
||||
|
||||
it('handles customer with special characters in name', function () {
|
||||
$specialName = "O'Brien & Co. - Test";
|
||||
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Customer')
|
||||
->set('form.name', $specialName)
|
||||
->set('form.gender', Gender::MALE->value)
|
||||
->call('create')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$this->assertDatabaseHas('customers', ['name' => $specialName]);
|
||||
});
|
||||
|
||||
it('handles multiple customers with same phone number', function () {
|
||||
$phoneNumber = '0812 3456 7890';
|
||||
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Customer')
|
||||
->set('form.name', 'Customer 1')
|
||||
->set('form.phone_number', $phoneNumber)
|
||||
->set('form.gender', Gender::MALE->value)
|
||||
->call('create')
|
||||
->assertHasNoErrors();
|
||||
|
||||
mountCustomerComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Customer')
|
||||
->set('form.name', 'Customer 2')
|
||||
->set('form.phone_number', $phoneNumber)
|
||||
->set('form.gender', Gender::FEMALE->value)
|
||||
->call('create')
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect(Customer::where('phone_number', $phoneNumber)->count())->toBe(2);
|
||||
});
|
||||
|
||||
it('handles empty stats when no customers exist', function () {
|
||||
$component = mountCustomerComponent($this->user);
|
||||
|
||||
expect($component->stats[0]['value'])->toBe(0);
|
||||
expect($component->stats[1]['value'])->toBe(0);
|
||||
expect($component->stats[2]['value'])->toBeNull();
|
||||
});
|
||||
|
||||
it('handles customer deletion when customer does not exist', function () {
|
||||
$customer = Customer::factory()->create();
|
||||
$customerId = $customer->id;
|
||||
$customer->delete();
|
||||
|
||||
// Try to delete already deleted customer
|
||||
$deletedCustomer = Customer::withTrashed()->find($customerId);
|
||||
|
||||
mountCustomerComponent($this->user)
|
||||
->call('delete', $deletedCustomer)
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect(Customer::withTrashed()->find($customerId)->trashed())->toBeTrue();
|
||||
});
|
||||
@ -1,649 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Studio\Loyalty\Tier;
|
||||
use App\Models\Bottle;
|
||||
use App\Models\Brand;
|
||||
use App\Models\Customer;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Membership;
|
||||
use App\Models\Order;
|
||||
use App\Models\OrderItem;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\Perfume;
|
||||
use App\Models\Product;
|
||||
use App\Models\Tier as TierModel;
|
||||
use App\Models\TierReward;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
|
||||
$this->ownerRole = Role::where('name', 'Owner')->first();
|
||||
|
||||
$this->user->roles()->attach($this->ownerRole->id);
|
||||
});
|
||||
|
||||
function mountTierComponent(User $user)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(Tier::class);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders page successfully', function () {
|
||||
mountTierComponent($this->user)
|
||||
->assertViewIs('livewire.studio.loyalty.tiers')
|
||||
->assertViewHas('pageTitle', 'Tier');
|
||||
});
|
||||
|
||||
it('loads tiers on mount', function () {
|
||||
$tier1 = TierModel::factory()->create(['name' => 'Bronze', 'min_spending' => 0, 'max_spending' => 100000]);
|
||||
$tier2 = TierModel::factory()->create(['name' => 'Silver', 'min_spending' => 100001, 'max_spending' => 500000]);
|
||||
|
||||
$component = mountTierComponent($this->user);
|
||||
|
||||
expect($component->tiers)->toBeInstanceOf(\Illuminate\Support\Collection::class);
|
||||
expect($component->tiers)->toHaveCount(2);
|
||||
expect($component->tiers[0]->name)->toBe('Bronze');
|
||||
expect($component->tiers[1]->name)->toBe('Silver');
|
||||
});
|
||||
|
||||
it('displays all tiers in the view', function () {
|
||||
$tier = TierModel::factory()->create(['name' => 'Gold', 'min_spending' => 1000000, 'max_spending' => 5000000]);
|
||||
|
||||
mountTierComponent($this->user)
|
||||
->assertSee('Gold')
|
||||
->assertSee('1.000.000');
|
||||
});
|
||||
|
||||
it('sorts tiers by min_spending ascending', function () {
|
||||
$tier3 = TierModel::factory()->create(['name' => 'Platinum', 'min_spending' => 10000000, 'max_spending' => null]);
|
||||
$tier1 = TierModel::factory()->create(['name' => 'Bronze', 'min_spending' => 0, 'max_spending' => 1000000]);
|
||||
$tier2 = TierModel::factory()->create(['name' => 'Silver', 'min_spending' => 1000001, 'max_spending' => 5000000]);
|
||||
|
||||
$component = mountTierComponent($this->user);
|
||||
|
||||
expect($component->tiers)->toBeInstanceOf(\Illuminate\Support\Collection::class);
|
||||
expect($component->tiers[0]->name)->toBe('Bronze');
|
||||
expect($component->tiers[1]->name)->toBe('Silver');
|
||||
expect($component->tiers[2]->name)->toBe('Platinum');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Create Tier
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('prevents create when user is unauthorized', function () {
|
||||
$this->ownerRole->revokePermissionTo('create tier');
|
||||
|
||||
Gate::define('create tier', fn () => false);
|
||||
|
||||
mountTierComponent($this->user)
|
||||
->set('form.name', 'Gold')
|
||||
->set('form.min_spending', '1000000')
|
||||
->set('form.max_spending', '5000000')
|
||||
->call('create')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('can create a tier when authorized', function () {
|
||||
mountTierComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Tier')
|
||||
->set('form.name', 'Gold')
|
||||
->set('form.min_spending', '1.000.000')
|
||||
->set('form.max_spending', '5.000.000')
|
||||
->call('create')
|
||||
->assertHasNoErrors()
|
||||
->assertSet('tiers', function ($tiers) {
|
||||
return $tiers instanceof \Illuminate\Support\Collection &&
|
||||
$tiers->count() === 1 &&
|
||||
$tiers[0]->name === 'Gold';
|
||||
});
|
||||
|
||||
$this->assertDatabaseHas('tiers', [
|
||||
'name' => 'Gold',
|
||||
'min_spending' => 1000000,
|
||||
'max_spending' => null, // reorderTierChain makes last tier null
|
||||
]);
|
||||
});
|
||||
|
||||
it('can create a tier without max_spending', function () {
|
||||
mountTierComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Tier')
|
||||
->set('form.name', 'Platinum')
|
||||
->set('form.min_spending', '5.000.001')
|
||||
->set('form.max_spending', null)
|
||||
->call('create')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$this->assertDatabaseHas('tiers', [
|
||||
'name' => 'Platinum',
|
||||
'min_spending' => 5000001,
|
||||
'max_spending' => null,
|
||||
]);
|
||||
});
|
||||
|
||||
it('sorts tiers after creating new tier', function () {
|
||||
TierModel::factory()->create(['name' => 'Silver', 'min_spending' => 500001, 'max_spending' => 1000000]);
|
||||
TierModel::factory()->create(['name' => 'Bronze', 'min_spending' => 0, 'max_spending' => 500000]);
|
||||
|
||||
$component = mountTierComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Tier')
|
||||
->set('form.name', 'Gold')
|
||||
->set('form.min_spending', '1.000.001')
|
||||
->set('form.max_spending', '2.000.000')
|
||||
->call('create');
|
||||
|
||||
expect($component->tiers)->toBeInstanceOf(\Illuminate\Support\Collection::class);
|
||||
expect($component->tiers[0]->name)->toBe('Bronze');
|
||||
expect($component->tiers[1]->name)->toBe('Silver');
|
||||
expect($component->tiers[2]->name)->toBe('Gold');
|
||||
});
|
||||
|
||||
it('validates required fields when creating tier', function () {
|
||||
mountTierComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Tier')
|
||||
->call('create')
|
||||
->assertHasErrors([
|
||||
'form.name',
|
||||
'form.min_spending',
|
||||
]);
|
||||
});
|
||||
|
||||
it('validates name field max length when creating tier', function () {
|
||||
mountTierComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Tier')
|
||||
->set('form.name', str_repeat('a', 51))
|
||||
->set('form.min_spending', '100')
|
||||
->call('create')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates min_spending is unsigned integer when creating tier', function () {
|
||||
mountTierComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Tier')
|
||||
->set('form.name', 'Gold')
|
||||
->set('form.min_spending', '-100')
|
||||
->call('create')
|
||||
->assertHasErrors(['form.min_spending']);
|
||||
});
|
||||
|
||||
it('validates max_spending is unsigned integer when creating tier', function () {
|
||||
mountTierComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Tier')
|
||||
->set('form.name', 'Gold')
|
||||
->set('form.min_spending', '100')
|
||||
->set('form.max_spending', '-50')
|
||||
->call('create')
|
||||
->assertHasErrors(['form.max_spending']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Update Tier
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('prevents update when user is unauthorized', function () {
|
||||
$this->ownerRole->revokePermissionTo('update tier');
|
||||
Gate::define('update tier', fn () => false);
|
||||
|
||||
$tier = TierModel::factory()->create(['name' => 'Bronze', 'min_spending' => 0, 'max_spending' => 100000]);
|
||||
|
||||
mountTierComponent($this->user)
|
||||
->call('openModal', 'update', 'Ubah Tier', $tier->hash)
|
||||
->set('form.name', 'Gold')
|
||||
->call('update')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('can update a tier when authorized', function () {
|
||||
$tier = TierModel::factory()->create(['name' => 'Bronze', 'min_spending' => 0, 'max_spending' => 100000]);
|
||||
|
||||
mountTierComponent($this->user)
|
||||
->call('openModal', 'update', 'Ubah Tier', $tier->hash)
|
||||
->assertSet('form.name', 'Bronze')
|
||||
->assertSet('form.min_spending', formatCurrencyNumber($tier->min_spending))
|
||||
->assertSet('form.max_spending', formatCurrencyNumber($tier->max_spending))
|
||||
->set('form.name', 'Gold')
|
||||
->set('form.min_spending', '1.000.000')
|
||||
->set('form.max_spending', '5.000.000')
|
||||
->call('update')
|
||||
->assertHasNoErrors()
|
||||
->assertSet('tiers', function ($tiers) use ($tier) {
|
||||
$updatedTier = $tiers->firstWhere('hash', $tier->hash);
|
||||
|
||||
return $updatedTier->name === 'Gold';
|
||||
});
|
||||
|
||||
$tier->refresh();
|
||||
expect($tier->name)->toBe('Gold');
|
||||
expect($tier->min_spending)->toBe(1000000);
|
||||
expect($tier->max_spending)->toBeNull(); // reorderTierChain makes last tier null
|
||||
});
|
||||
|
||||
it('can update tier without max_spending', function () {
|
||||
$tier = TierModel::factory()->create(['name' => 'Bronze', 'min_spending' => 0, 'max_spending' => 100000]);
|
||||
|
||||
mountTierComponent($this->user)
|
||||
->call('openModal', 'update', 'Ubah Tier', $tier->hash)
|
||||
->set('form.max_spending', null)
|
||||
->call('update')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$tier->refresh();
|
||||
expect($tier->max_spending)->toBeNull();
|
||||
});
|
||||
|
||||
it('validates required fields when updating tier', function () {
|
||||
$tier = TierModel::factory()->create();
|
||||
|
||||
mountTierComponent($this->user)
|
||||
->call('openModal', 'update', 'Ubah Tier', $tier->hash)
|
||||
->set('form.name', '')
|
||||
->set('form.min_spending', '')
|
||||
->call('update')
|
||||
->assertHasErrors([
|
||||
'form.name',
|
||||
'form.min_spending',
|
||||
]);
|
||||
});
|
||||
|
||||
it('validates name field max length when updating tier', function () {
|
||||
$tier = TierModel::factory()->create();
|
||||
|
||||
mountTierComponent($this->user)
|
||||
->call('openModal', 'update', 'Ubah Tier', $tier->hash)
|
||||
->set('form.name', str_repeat('a', 51))
|
||||
->call('update')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('resets validation and error bag when opening modal', function () {
|
||||
$tier = TierModel::factory()->create();
|
||||
|
||||
$component = mountTierComponent($this->user)
|
||||
->call('openModal', 'update', 'Ubah Tier', $tier->hash)
|
||||
->set('form.name', '')
|
||||
->call('update')
|
||||
->assertHasErrors(['form.name']);
|
||||
|
||||
$component->call('openModal', 'update', 'Ubah Tier', $tier->hash)
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Delete Tier
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('can delete a tier when authorized', function () {
|
||||
$tier = TierModel::factory()->create(['name' => 'Bronze']);
|
||||
|
||||
mountTierComponent($this->user)
|
||||
->call('delete', $tier)
|
||||
->assertHasNoErrors()
|
||||
->assertSet('tiers', function ($tiers) use ($tier) {
|
||||
return ! $tiers->contains(function ($t) use ($tier) {
|
||||
return $t->hash === $tier->hash;
|
||||
});
|
||||
});
|
||||
|
||||
expect(TierModel::count())->toBe(0);
|
||||
$this->assertSoftDeleted('tiers', ['id' => $tier->id]);
|
||||
});
|
||||
|
||||
it('cascades delete to memberships when deleting tier', function () {
|
||||
$tier = TierModel::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$customer = Customer::factory()->create(['user_id' => $user->id]);
|
||||
$membership = Membership::create([
|
||||
'user_id' => $user->id,
|
||||
'tier_id' => $tier->id,
|
||||
'total_spending' => 100000,
|
||||
]);
|
||||
|
||||
mountTierComponent($this->user)
|
||||
->call('delete', $tier)
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect(Membership::count())->toBe(0);
|
||||
$this->assertSoftDeleted('memberships', ['id' => $membership->id]);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Load Tiers
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('loads tiers with membership count', function () {
|
||||
$tier = TierModel::factory()->create();
|
||||
$user1 = User::factory()->create();
|
||||
$user2 = User::factory()->create();
|
||||
$customer1 = Customer::factory()->create(['user_id' => $user1->id]);
|
||||
$customer2 = Customer::factory()->create(['user_id' => $user2->id]);
|
||||
|
||||
Membership::create(['user_id' => $user1->id, 'tier_id' => $tier->id, 'total_spending' => 100000]);
|
||||
Membership::create(['user_id' => $user2->id, 'tier_id' => $tier->id, 'total_spending' => 200000]);
|
||||
|
||||
$component = mountTierComponent($this->user);
|
||||
|
||||
$loadedTier = $component->tiers->firstWhere('id', $tier->id);
|
||||
expect($loadedTier->total_members)->toBe('2');
|
||||
});
|
||||
|
||||
it('loads tiers with rewards count', function () {
|
||||
$tier = TierModel::factory()->create();
|
||||
TierReward::create(['tier_id' => $tier->id, 'name' => 'Reward 1', 'points' => 100]);
|
||||
TierReward::create(['tier_id' => $tier->id, 'name' => 'Reward 2', 'points' => 200]);
|
||||
TierReward::create(['tier_id' => $tier->id, 'name' => 'Reward 3', 'points' => 300]);
|
||||
|
||||
$component = mountTierComponent($this->user);
|
||||
|
||||
$loadedTier = $component->tiers->firstWhere('id', $tier->id);
|
||||
|
||||
expect($loadedTier['total_rewards'])->toBe('3');
|
||||
});
|
||||
|
||||
it('loads top member stats when tier has memberships', function () {
|
||||
$outlet = Outlet::factory()->operational()->create();
|
||||
$tier = TierModel::factory()->create(['name' => 'Gold']);
|
||||
$user1 = User::factory()->create();
|
||||
$user2 = User::factory()->create();
|
||||
$customer1 = Customer::factory()->create(['user_id' => $user1->id, 'name' => 'John Doe']);
|
||||
$customer2 = Customer::factory()->create(['user_id' => $user2->id, 'name' => 'Jane Doe']);
|
||||
|
||||
// Create memberships with different total_spending
|
||||
$membership1 = Membership::create([
|
||||
'user_id' => $user1->id,
|
||||
'tier_id' => $tier->id,
|
||||
'total_spending' => 5000000, // Higher spending
|
||||
]);
|
||||
|
||||
$membership2 = Membership::create([
|
||||
'user_id' => $user2->id,
|
||||
'tier_id' => $tier->id,
|
||||
'total_spending' => 2000000, // Lower spending
|
||||
]);
|
||||
|
||||
// Create orders for user1 (top member)
|
||||
$order1 = Order::create([
|
||||
'outlet_id' => $outlet->id,
|
||||
'customer_id' => $customer1->id,
|
||||
'user_id' => $user1->id,
|
||||
'invoice_number' => 'INV-001',
|
||||
'cogs' => 50000,
|
||||
'subtotal' => 100000,
|
||||
'discount' => 10000,
|
||||
'total' => 90000,
|
||||
'ordered_at' => now(),
|
||||
]);
|
||||
|
||||
$order2 = Order::create([
|
||||
'outlet_id' => $outlet->id,
|
||||
'customer_id' => $customer1->id,
|
||||
'user_id' => $user1->id,
|
||||
'invoice_number' => 'INV-002',
|
||||
'cogs' => 25000,
|
||||
'subtotal' => 50000,
|
||||
'discount' => 5000,
|
||||
'total' => 45000,
|
||||
'ordered_at' => now(),
|
||||
]);
|
||||
|
||||
// Create order items for order1
|
||||
$brand = Brand::factory()->create();
|
||||
$perfume1 = Perfume::create([
|
||||
'brand_id' => $brand->id,
|
||||
'name' => 'Test Perfume 1',
|
||||
'slug' => 'test-perfume-1',
|
||||
'sku' => 'TP1-0001',
|
||||
'cost_price' => 10000,
|
||||
'sale_price' => 50000,
|
||||
]);
|
||||
$product1 = Product::create([
|
||||
'name' => 'Test Product 1',
|
||||
'slug' => 'test-product-1',
|
||||
'sku' => 'PR1-0001',
|
||||
'cost_price' => 5000,
|
||||
'sale_price' => 25000,
|
||||
]);
|
||||
$bottle1 = Bottle::factory()->create();
|
||||
|
||||
OrderItem::create([
|
||||
'order_id' => $order1->id,
|
||||
'user_id' => $user1->id,
|
||||
'orderable_type' => Perfume::class,
|
||||
'orderable_id' => $perfume1->id,
|
||||
'cogs' => 10000,
|
||||
'unit_price' => 50000,
|
||||
'quantity' => 2,
|
||||
]);
|
||||
|
||||
OrderItem::create([
|
||||
'order_id' => $order1->id,
|
||||
'user_id' => $user1->id,
|
||||
'orderable_type' => Product::class,
|
||||
'orderable_id' => $product1->id,
|
||||
'cogs' => 5000,
|
||||
'unit_price' => 25000,
|
||||
'quantity' => 3,
|
||||
]);
|
||||
|
||||
OrderItem::create([
|
||||
'order_id' => $order1->id,
|
||||
'user_id' => $user1->id,
|
||||
'orderable_type' => Bottle::class,
|
||||
'orderable_id' => $bottle1->id,
|
||||
'cogs' => 3000,
|
||||
'unit_price' => 15000,
|
||||
'quantity' => 1,
|
||||
]);
|
||||
|
||||
// Create order items for order2
|
||||
$perfume2 = Perfume::create([
|
||||
'brand_id' => $brand->id,
|
||||
'name' => 'Test Perfume 2',
|
||||
'slug' => 'test-perfume-2',
|
||||
'sku' => 'TP2-0001',
|
||||
'cost_price' => 10000,
|
||||
'sale_price' => 50000,
|
||||
]);
|
||||
OrderItem::create([
|
||||
'order_id' => $order2->id,
|
||||
'user_id' => $user1->id,
|
||||
'orderable_type' => Perfume::class,
|
||||
'orderable_id' => $perfume2->id,
|
||||
'cogs' => 10000,
|
||||
'unit_price' => 50000,
|
||||
'quantity' => 1,
|
||||
]);
|
||||
|
||||
$component = mountTierComponent($this->user);
|
||||
|
||||
$loadedTier = $component->tiers->firstWhere('id', $tier->id);
|
||||
expect($loadedTier->top_member)->not->toBeNull();
|
||||
expect($loadedTier->top_member['name'])->toBe('John Doe');
|
||||
expect($loadedTier->top_member['spending'])->toContain('5.000.000');
|
||||
expect($loadedTier->top_member['total_orders'])->toBe('2');
|
||||
expect($loadedTier->top_member['items']['total_perfume'])->toBe('3');
|
||||
expect($loadedTier->top_member['items']['total_product'])->toContain('3');
|
||||
expect($loadedTier->top_member['items']['total_bottle'])->toContain('1');
|
||||
});
|
||||
|
||||
it('shows null top member when tier has no memberships', function () {
|
||||
$tier = TierModel::factory()->create();
|
||||
|
||||
$component = mountTierComponent($this->user);
|
||||
|
||||
$loadedTier = $component->tiers->firstWhere('id', $tier->id);
|
||||
expect($loadedTier->top_member)->toBeNull();
|
||||
});
|
||||
|
||||
it('reloads tiers when data:tiersUpdated event is dispatched', function () {
|
||||
$tier1 = TierModel::factory()->create(['name' => 'Bronze']);
|
||||
$component = mountTierComponent($this->user);
|
||||
|
||||
expect($component->tiers)->toHaveCount(1);
|
||||
|
||||
$tier2 = TierModel::factory()->create(['name' => 'Silver']);
|
||||
|
||||
$component->dispatch('data:tiersUpdated')
|
||||
->assertSet('tiers', function ($tiers) {
|
||||
return $tiers->count() === 2;
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Modal Management
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('opens modal with correct method and title for create', function () {
|
||||
$component = mountTierComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Tier');
|
||||
|
||||
expect($component->method)->toBe('create');
|
||||
expect($component->modalTitle)->toBe('Tambah Tier');
|
||||
});
|
||||
|
||||
it('opens modal with correct method and title for update', function () {
|
||||
$tier = TierModel::factory()->create();
|
||||
|
||||
$component = mountTierComponent($this->user)
|
||||
->call('openModal', 'update', 'Ubah Tier', $tier->hash);
|
||||
|
||||
expect($component->method)->toBe('update');
|
||||
expect($component->modalTitle)->toBe('Ubah Tier');
|
||||
expect($component->form->name)->toBe($tier->name);
|
||||
expect($component->form->min_spending)->toBe(formatCurrencyNumber($tier->min_spending));
|
||||
expect($component->form->max_spending)->toBe($tier->max_spending ? formatCurrencyNumber($tier->max_spending) : null);
|
||||
});
|
||||
|
||||
it('sets form data when opening update modal', function () {
|
||||
$tier = TierModel::factory()->create([
|
||||
'name' => 'Gold',
|
||||
'min_spending' => 1000000,
|
||||
'max_spending' => 5000000,
|
||||
]);
|
||||
|
||||
$component = mountTierComponent($this->user)
|
||||
->call('openModal', 'update', 'Ubah Tier', $tier->hash);
|
||||
|
||||
expect($component->form->name)->toBe('Gold');
|
||||
expect($component->form->min_spending)->toBe('1.000.000');
|
||||
expect($component->form->max_spending)->toBe('5.000.000');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Form Data Formatting
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('formats spending with currency when displaying', function () {
|
||||
$tier = TierModel::factory()->create([
|
||||
'min_spending' => 1000000,
|
||||
'max_spending' => 5000000,
|
||||
]);
|
||||
|
||||
$component = mountTierComponent($this->user);
|
||||
|
||||
$loadedTier = $component->tiers->firstWhere('id', $tier->id);
|
||||
expect(formatCurrencyNumber($loadedTier->min_spending))->toContain('1.000.000');
|
||||
expect(formatCurrencyNumber($loadedTier->max_spending))->toContain('5.000.000');
|
||||
});
|
||||
|
||||
it('handles currency formatting in form input', function () {
|
||||
mountTierComponent($this->user)
|
||||
->call('openModal', 'create', 'Tambah Tier')
|
||||
->set('form.name', 'Gold')
|
||||
->set('form.min_spending', '1.000.000')
|
||||
->set('form.max_spending', '5.000.000')
|
||||
->call('create')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$tier = TierModel::first();
|
||||
expect($tier->min_spending)->toBe(1000000);
|
||||
expect($tier->max_spending)->toBeNull(); // ReorderTierChain makes the last tier null
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Edge Cases
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('handles tier with null max_spending correctly', function () {
|
||||
$tier = TierModel::factory()->create([
|
||||
'name' => 'Platinum',
|
||||
'min_spending' => 10000000,
|
||||
'max_spending' => null,
|
||||
]);
|
||||
|
||||
$component = mountTierComponent($this->user);
|
||||
|
||||
$loadedTier = $component->tiers->firstWhere('id', $tier->id);
|
||||
expect(formatCurrencyNumber($loadedTier->max_spending))->not->toBeNull();
|
||||
});
|
||||
|
||||
it('handles tier with zero min_spending', function () {
|
||||
$tier = TierModel::factory()->create([
|
||||
'name' => 'Starter',
|
||||
'min_spending' => 0,
|
||||
'max_spending' => 100000,
|
||||
]);
|
||||
|
||||
$component = mountTierComponent($this->user);
|
||||
|
||||
$loadedTier = $component->tiers->firstWhere('id', $tier->id);
|
||||
expect(formatCurrencyNumber($loadedTier->min_spending))->toContain('0');
|
||||
});
|
||||
|
||||
it('handles empty tiers list', function () {
|
||||
$component = mountTierComponent($this->user);
|
||||
|
||||
expect($component->tiers)->toBeInstanceOf(Collection::class);
|
||||
});
|
||||
|
||||
it('updates tier array correctly after update', function () {
|
||||
$tier = TierModel::factory()->create([
|
||||
'name' => 'Bronze',
|
||||
'min_spending' => 0,
|
||||
'max_spending' => 100000,
|
||||
]);
|
||||
|
||||
$component = mountTierComponent($this->user)
|
||||
->call('openModal', 'update', 'Ubah Tier', $tier->hash)
|
||||
->set('form.name', 'Gold')
|
||||
->set('form.min_spending', '1.000.000')
|
||||
->set('form.max_spending', '5.000.000')
|
||||
->call('update');
|
||||
|
||||
$updatedTier = $component->tiers->firstWhere('hash', $tier->hash);
|
||||
expect($updatedTier->name)->toBe('Gold');
|
||||
expect($updatedTier->formatted_min_spending)->toContain('1.000.000');
|
||||
expect($updatedTier->max_spending)->toBeNull(); // ReorderTierChain makes the last tier null
|
||||
});
|
||||
@ -1,596 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\VoucherType;
|
||||
use App\Jobs\StorePushNotification;
|
||||
use App\Livewire\Studio\Loyalty\Voucher\Create;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Facility;
|
||||
use App\Models\OpeningHour;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\PushNotification;
|
||||
use App\Models\User;
|
||||
use App\Models\Voucher;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
|
||||
$this->ownerRole = Role::where('name', 'Owner')->first();
|
||||
|
||||
$this->user->roles()->attach($this->ownerRole->id);
|
||||
|
||||
Bus::fake();
|
||||
});
|
||||
|
||||
function mountCreateComponent(User $user)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(Create::class);
|
||||
}
|
||||
|
||||
function createOutlets(int $count = 3)
|
||||
{
|
||||
return Outlet::factory()
|
||||
->count($count)
|
||||
->has(OpeningHour::factory()->count(7))
|
||||
->has(Facility::factory()->count(5))
|
||||
->create();
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders page successfully', function () {
|
||||
mountCreateComponent($this->user)
|
||||
->assertViewIs('livewire.studio.loyalty.voucher.form')
|
||||
->assertViewHas('pageTitle', 'Tambah Voucher');
|
||||
});
|
||||
|
||||
it('loads outlets on mount', function () {
|
||||
$outlets = createOutlets(3);
|
||||
|
||||
$component = mountCreateComponent($this->user);
|
||||
|
||||
expect($component->outlets)->toHaveCount(3);
|
||||
expect($component->outlets)->toHaveKey($outlets->first()->id);
|
||||
expect($component->outlets[$outlets->first()->id])->toBe($outlets->first()->name);
|
||||
});
|
||||
|
||||
it('initializes form with default values', function () {
|
||||
$component = mountCreateComponent($this->user);
|
||||
|
||||
expect($component->form->name)->toBe('');
|
||||
expect($component->form->code)->toBe('');
|
||||
expect($component->form->tags)->toBe('');
|
||||
expect($component->form->type)->toBe('1');
|
||||
expect($component->form->discount_amount)->toBe('');
|
||||
expect($component->form->outlet_ids)->toBe([]);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Create Voucher - Fixed Type
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('can create a fixed type voucher with all fields', function () {
|
||||
$outlets = createOutlets(3);
|
||||
$outletIds = $outlets->pluck('id')->toArray();
|
||||
|
||||
$component = mountCreateComponent($this->user)
|
||||
->set('form.name', 'Voucher Diskon 10rb')
|
||||
->set('form.code', 'DISKON10')
|
||||
->set('form.tags', 'Member Baru')
|
||||
->set('form.type', VoucherType::FIXED->value)
|
||||
->set('form.discount_amount', '10000')
|
||||
->set('form.min_purchase', '50000')
|
||||
->set('form.quota', '100')
|
||||
->set('form.limit_per_user', '2')
|
||||
->set('form.summary', 'Diskon 10rb untuk pembelian minimal 50rb')
|
||||
->set('form.start_date', now()->addDay()->format('Y-m-d'))
|
||||
->set('form.end_date', now()->addMonth()->format('Y-m-d'))
|
||||
->set('form.outlet_ids', $outletIds)
|
||||
->call('save');
|
||||
|
||||
$component->assertHasNoErrors()
|
||||
->assertRedirect(route('studio.loyalty.voucher.index'));
|
||||
|
||||
$this->assertDatabaseHas('vouchers', [
|
||||
'name' => 'Voucher Diskon 10rb',
|
||||
'code' => 'DISKON10',
|
||||
'tags' => 'Member Baru',
|
||||
'type' => VoucherType::FIXED->value,
|
||||
'discount_amount' => 10000,
|
||||
'min_purchase' => 50000,
|
||||
'quota' => 100,
|
||||
'available_count' => 100,
|
||||
'limit_per_user' => 2,
|
||||
'summary' => 'Diskon 10rb untuk pembelian minimal 50rb',
|
||||
'max_discount' => null,
|
||||
]);
|
||||
|
||||
$voucher = Voucher::where('code', 'DISKON10')->first();
|
||||
|
||||
expect($voucher->outlets)->toHaveCount(3);
|
||||
expect($voucher->outlets->pluck('id')->toArray())->toEqual($outletIds);
|
||||
});
|
||||
|
||||
it('can create a fixed type voucher without optional fields', function () {
|
||||
$outlets = createOutlets(1);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', 'Voucher Gratis')
|
||||
->set('form.code', 'GRATIS')
|
||||
->set('form.tags', 'Promo')
|
||||
->set('form.type', VoucherType::FIXED->value)
|
||||
->set('form.discount_amount', '5000')
|
||||
->set('form.summary', 'Voucher gratis')
|
||||
->set('form.start_date', now()->addDay()->format('Y-m-d'))
|
||||
->set('form.outlet_ids', [$outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$this->assertDatabaseHas('vouchers', [
|
||||
'code' => 'GRATIS',
|
||||
'min_purchase' => null,
|
||||
'max_discount' => null,
|
||||
'quota' => null,
|
||||
'available_count' => 0,
|
||||
'limit_per_user' => null,
|
||||
'end_date' => null,
|
||||
]);
|
||||
});
|
||||
|
||||
it('can create a fixed type voucher with currency formatted amounts', function () {
|
||||
$outlets = createOutlets(1);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', 'Voucher Test')
|
||||
->set('form.code', 'TEST01')
|
||||
->set('form.tags', 'Test')
|
||||
->set('form.type', VoucherType::FIXED->value)
|
||||
->set('form.discount_amount', '25.000')
|
||||
->set('form.min_purchase', '100.000')
|
||||
->set('form.summary', 'Test voucher')
|
||||
->set('form.start_date', now()->addDay()->format('Y-m-d'))
|
||||
->set('form.outlet_ids', [$outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$this->assertDatabaseHas('vouchers', [
|
||||
'code' => 'TEST01',
|
||||
'discount_amount' => 25000,
|
||||
'min_purchase' => 100000,
|
||||
]);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Create Voucher - Percentage Type
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('can create a percentage type voucher with max discount', function () {
|
||||
$outlets = createOutlets(2);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', 'Diskon 20%')
|
||||
->set('form.code', 'DISKON20')
|
||||
->set('form.tags', 'Promo')
|
||||
->set('form.type', VoucherType::PERCENTAGE->value)
|
||||
->set('form.discount_amount', '20')
|
||||
->set('form.max_discount', '50000')
|
||||
->set('form.min_purchase', '100000')
|
||||
->set('form.quota', '200')
|
||||
->set('form.limit_per_user', '1')
|
||||
->set('form.summary', 'Diskon 20% maksimal 50rb')
|
||||
->set('form.start_date', now()->addDay()->format('Y-m-d'))
|
||||
->set('form.end_date', now()->addMonths(2)->format('Y-m-d'))
|
||||
->set('form.outlet_ids', $outlets->pluck('id')->toArray())
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$this->assertDatabaseHas('vouchers', [
|
||||
'code' => 'DISKON20',
|
||||
'type' => VoucherType::PERCENTAGE->value,
|
||||
'discount_amount' => 20,
|
||||
'max_discount' => 50000,
|
||||
'quota' => 200,
|
||||
'available_count' => 200,
|
||||
]);
|
||||
});
|
||||
|
||||
it('requires max_discount for percentage type voucher', function () {
|
||||
$outlets = createOutlets(1);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', 'Diskon Test')
|
||||
->set('form.code', 'TEST02')
|
||||
->set('form.tags', 'Test')
|
||||
->set('form.type', VoucherType::PERCENTAGE->value)
|
||||
->set('form.discount_amount', '15')
|
||||
->set('form.summary', 'Test')
|
||||
->set('form.start_date', now()->addDay()->format('Y-m-d'))
|
||||
->set('form.outlet_ids', [$outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.max_discount']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Validation Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates required fields', function () {
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.type', '')
|
||||
->call('save')
|
||||
->assertHasErrors([
|
||||
'form.name',
|
||||
'form.code',
|
||||
'form.tags',
|
||||
'form.type',
|
||||
'form.discount_amount',
|
||||
'form.summary',
|
||||
'form.start_date',
|
||||
'form.outlet_ids',
|
||||
]);
|
||||
});
|
||||
|
||||
it('validates name max length', function () {
|
||||
$outlets = createOutlets(1);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', str_repeat('a', 51))
|
||||
->set('form.code', 'TEST')
|
||||
->set('form.tags', 'Test')
|
||||
->set('form.type', VoucherType::FIXED->value)
|
||||
->set('form.discount_amount', '10000')
|
||||
->set('form.summary', 'Test')
|
||||
->set('form.start_date', now()->addDay()->format('Y-m-d'))
|
||||
->set('form.outlet_ids', [$outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates code uniqueness', function () {
|
||||
$voucher = Voucher::factory()->create(['code' => 'EXISTING']);
|
||||
$outlets = createOutlets(1);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', 'New Voucher')
|
||||
->set('form.code', 'EXISTING')
|
||||
->set('form.tags', 'Test')
|
||||
->set('form.type', VoucherType::FIXED->value)
|
||||
->set('form.discount_amount', '10000')
|
||||
->set('form.summary', 'Test')
|
||||
->set('form.start_date', now()->addDay()->format('Y-m-d'))
|
||||
->set('form.outlet_ids', [$outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.code']);
|
||||
});
|
||||
|
||||
it('validates code format (alpha_num)', function () {
|
||||
$outlets = createOutlets(1);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', 'Test')
|
||||
->set('form.code', 'TEST-CODE')
|
||||
->set('form.tags', 'Test')
|
||||
->set('form.type', VoucherType::FIXED->value)
|
||||
->set('form.discount_amount', '10000')
|
||||
->set('form.summary', 'Test')
|
||||
->set('form.start_date', now()->addDay()->format('Y-m-d'))
|
||||
->set('form.outlet_ids', [$outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.code']);
|
||||
});
|
||||
|
||||
it('validates tags max length', function () {
|
||||
$outlets = createOutlets(1);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', 'Test')
|
||||
->set('form.code', 'TEST')
|
||||
->set('form.tags', str_repeat('a', 21))
|
||||
->set('form.type', VoucherType::FIXED->value)
|
||||
->set('form.discount_amount', '10000')
|
||||
->set('form.summary', 'Test')
|
||||
->set('form.start_date', now()->addDay()->format('Y-m-d'))
|
||||
->set('form.outlet_ids', [$outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.tags']);
|
||||
});
|
||||
|
||||
it('validates percentage discount amount range', function () {
|
||||
$outlets = createOutlets(1);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', 'Test')
|
||||
->set('form.code', 'TEST')
|
||||
->set('form.tags', 'Test')
|
||||
->set('form.type', VoucherType::PERCENTAGE->value)
|
||||
->set('form.discount_amount', '101')
|
||||
->set('form.max_discount', '50000')
|
||||
->set('form.summary', 'Test')
|
||||
->set('form.start_date', now()->addDay()->format('Y-m-d'))
|
||||
->set('form.outlet_ids', [$outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.discount_amount']);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', 'Test')
|
||||
->set('form.code', 'TEST2')
|
||||
->set('form.tags', 'Test')
|
||||
->set('form.type', VoucherType::PERCENTAGE->value)
|
||||
->set('form.discount_amount', '-1')
|
||||
->set('form.max_discount', '50000')
|
||||
->set('form.summary', 'Test')
|
||||
->set('form.start_date', now()->addDay()->format('Y-m-d'))
|
||||
->set('form.outlet_ids', [$outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.discount_amount']);
|
||||
});
|
||||
|
||||
it('validates start date is after yesterday', function () {
|
||||
$outlets = createOutlets(1);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', 'Test')
|
||||
->set('form.code', 'TEST')
|
||||
->set('form.tags', 'Test')
|
||||
->set('form.type', VoucherType::FIXED->value)
|
||||
->set('form.discount_amount', '10000')
|
||||
->set('form.summary', 'Test')
|
||||
->set('form.start_date', now()->subDays(2)->format('Y-m-d'))
|
||||
->set('form.outlet_ids', [$outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.start_date']);
|
||||
});
|
||||
|
||||
it('validates end date is after or equal to start date', function () {
|
||||
$outlets = createOutlets(1);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', 'Test')
|
||||
->set('form.code', 'TEST')
|
||||
->set('form.tags', 'Test')
|
||||
->set('form.type', VoucherType::FIXED->value)
|
||||
->set('form.discount_amount', '10000')
|
||||
->set('form.summary', 'Test')
|
||||
->set('form.start_date', now()->addDays(5)->format('Y-m-d'))
|
||||
->set('form.end_date', now()->addDays(3)->format('Y-m-d'))
|
||||
->set('form.outlet_ids', [$outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.end_date']);
|
||||
});
|
||||
|
||||
it('validates outlet_ids is required and has at least one', function () {
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', 'Test')
|
||||
->set('form.code', 'TEST')
|
||||
->set('form.tags', 'Test')
|
||||
->set('form.type', VoucherType::FIXED->value)
|
||||
->set('form.discount_amount', '10000')
|
||||
->set('form.summary', 'Test')
|
||||
->set('form.start_date', now()->addDay()->format('Y-m-d'))
|
||||
->set('form.outlet_ids', [])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.outlet_ids']);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', 'Test')
|
||||
->set('form.code', 'TEST2')
|
||||
->set('form.tags', 'Test')
|
||||
->set('form.type', VoucherType::FIXED->value)
|
||||
->set('form.discount_amount', '10000')
|
||||
->set('form.summary', 'Test')
|
||||
->set('form.start_date', now()->addDay()->format('Y-m-d'))
|
||||
->call('save')
|
||||
->assertHasErrors(['form.outlet_ids']);
|
||||
});
|
||||
|
||||
it('validates outlet_ids exist', function () {
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', 'Test')
|
||||
->set('form.code', 'TEST')
|
||||
->set('form.tags', 'Test')
|
||||
->set('form.type', VoucherType::FIXED->value)
|
||||
->set('form.discount_amount', '10000')
|
||||
->set('form.summary', 'Test')
|
||||
->set('form.start_date', now()->addDay()->format('Y-m-d'))
|
||||
->set('form.outlet_ids', [99999])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.outlet_ids.0']);
|
||||
});
|
||||
|
||||
it('validates summary max length', function () {
|
||||
$outlets = createOutlets(1);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', 'Test')
|
||||
->set('form.code', 'TEST')
|
||||
->set('form.tags', 'Test')
|
||||
->set('form.type', VoucherType::FIXED->value)
|
||||
->set('form.discount_amount', '10000')
|
||||
->set('form.summary', str_repeat('a', 201))
|
||||
->set('form.start_date', now()->addDay()->format('Y-m-d'))
|
||||
->set('form.outlet_ids', [$outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.summary']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Outlet Selection
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('can select all outlets', function () {
|
||||
$outlets = createOutlets(5);
|
||||
|
||||
$component = mountCreateComponent($this->user);
|
||||
$component->call('selectAllOutlets');
|
||||
|
||||
expect($component->form->outlet_ids)->toHaveCount(5);
|
||||
expect($component->form->outlet_ids)->toEqual($outlets->pluck('id')->toArray());
|
||||
});
|
||||
|
||||
it('can deselect all outlets', function () {
|
||||
$outlets = createOutlets(3);
|
||||
|
||||
$component = mountCreateComponent($this->user)
|
||||
->set('form.outlet_ids', $outlets->pluck('id')->toArray());
|
||||
|
||||
expect($component->form->outlet_ids)->toHaveCount(3);
|
||||
|
||||
$component->call('deselectAllOutlets');
|
||||
|
||||
expect($component->form->outlet_ids)->toHaveCount(0);
|
||||
expect($component->form->outlet_ids)->toBe([]);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Permissions
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('prevents creation when user lacks create voucher permission', function () {
|
||||
$this->ownerRole->revokePermissionTo('create voucher');
|
||||
Gate::define('create voucher', fn () => false);
|
||||
|
||||
$outlets = createOutlets(1);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', 'Test')
|
||||
->set('form.code', 'TEST')
|
||||
->set('form.tags', 'Test')
|
||||
->set('form.type', VoucherType::FIXED->value)
|
||||
->set('form.discount_amount', '10000')
|
||||
->set('form.summary', 'Test')
|
||||
->set('form.start_date', now()->addDay()->format('Y-m-d'))
|
||||
->set('form.outlet_ids', [$outlets->first()->id])
|
||||
->call('save')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('allows creation when user has create voucher permission', function () {
|
||||
$outlets = createOutlets(1);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', 'Test')
|
||||
->set('form.code', 'TEST')
|
||||
->set('form.tags', 'Test')
|
||||
->set('form.type', VoucherType::FIXED->value)
|
||||
->set('form.discount_amount', '10000')
|
||||
->set('form.summary', 'Test')
|
||||
->set('form.start_date', now()->addDay()->format('Y-m-d'))
|
||||
->set('form.outlet_ids', [$outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Push Notification
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('dispatches push notification job when creating voucher', function () {
|
||||
$users = User::factory()->count(3)->create();
|
||||
foreach ($users as $user) {
|
||||
PushNotification::create([
|
||||
'user_id' => $user->id,
|
||||
'subscriptions' => json_encode(['endpoint' => 'test', 'keys' => ['p256dh' => 'test', 'auth' => 'test']]),
|
||||
]);
|
||||
}
|
||||
|
||||
$outlets = createOutlets(1);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', 'Test')
|
||||
->set('form.code', 'TEST')
|
||||
->set('form.tags', 'Test')
|
||||
->set('form.type', VoucherType::FIXED->value)
|
||||
->set('form.discount_amount', '10000')
|
||||
->set('form.summary', 'Test')
|
||||
->set('form.start_date', now()->addDay()->format('Y-m-d'))
|
||||
->set('form.outlet_ids', [$outlets->first()->id])
|
||||
->call('save');
|
||||
|
||||
Bus::assertDispatched(StorePushNotification::class, function ($job) {
|
||||
return $job->payload['title'] === '🎁 Voucher Baru!' &&
|
||||
$job->payload['body'] === 'Ada voucher baru untuk kamu! Jangan sampai kelewatan, yuk cek sekarang dan nikmati penawarannya ✨🎉';
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Edge Cases
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('handles null optional fields correctly', function () {
|
||||
$outlets = createOutlets(1);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', 'Test')
|
||||
->set('form.code', 'TEST')
|
||||
->set('form.tags', 'Test')
|
||||
->set('form.type', VoucherType::FIXED->value)
|
||||
->set('form.discount_amount', '10000')
|
||||
->set('form.min_purchase', null)
|
||||
->set('form.quota', null)
|
||||
->set('form.limit_per_user', null)
|
||||
->set('form.summary', 'Test')
|
||||
->set('form.start_date', now()->addDay()->format('Y-m-d'))
|
||||
->set('form.end_date', null)
|
||||
->set('form.outlet_ids', [$outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$voucher = Voucher::where('code', 'TEST')->first();
|
||||
|
||||
expect($voucher->min_purchase)->toBeNull();
|
||||
expect($voucher->quota)->toBeNull();
|
||||
expect($voucher->limit_per_user)->toBeNull();
|
||||
expect($voucher->end_date)->toBeNull();
|
||||
expect($voucher->available_count)->toBe(0);
|
||||
});
|
||||
|
||||
it('sets available_count equal to quota when quota is provided', function () {
|
||||
$outlets = createOutlets(1);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', 'Test')
|
||||
->set('form.code', 'TEST')
|
||||
->set('form.tags', 'Test')
|
||||
->set('form.type', VoucherType::FIXED->value)
|
||||
->set('form.discount_amount', '10000')
|
||||
->set('form.quota', '150')
|
||||
->set('form.summary', 'Test')
|
||||
->set('form.start_date', now()->addDay()->format('Y-m-d'))
|
||||
->set('form.outlet_ids', [$outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$voucher = Voucher::where('code', 'TEST')->first();
|
||||
expect($voucher->quota)->toBe(150);
|
||||
expect($voucher->available_count)->toBe(150);
|
||||
});
|
||||
@ -1,495 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\VoucherType;
|
||||
use App\Livewire\Studio\Loyalty\Voucher\Edit;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Facility;
|
||||
use App\Models\OpeningHour;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\User;
|
||||
use App\Models\Voucher;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
|
||||
$this->ownerRole = Role::where('name', 'Owner')->first();
|
||||
|
||||
$this->user->roles()->attach($this->ownerRole->id);
|
||||
});
|
||||
|
||||
function mountEditComponent(User $user, Voucher $voucher)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(Edit::class, ['voucher' => $voucher]);
|
||||
}
|
||||
|
||||
function createTestOutlets(int $count = 3)
|
||||
{
|
||||
return Outlet::factory()
|
||||
->count($count)
|
||||
->has(OpeningHour::factory()->count(7))
|
||||
->has(Facility::factory()->count(5))
|
||||
->create();
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders page successfully', function () {
|
||||
$voucher = Voucher::factory()->create();
|
||||
$voucher->outlets()->attach(createOutlets(2)->pluck('id'));
|
||||
|
||||
mountEditComponent($this->user, $voucher)
|
||||
->assertViewIs('livewire.studio.loyalty.voucher.form')
|
||||
->assertViewHas('pageTitle', 'Ubah Voucher');
|
||||
});
|
||||
|
||||
it('loads outlets on mount', function () {
|
||||
$voucher = Voucher::factory()->create();
|
||||
$outlets = createOutlets(3);
|
||||
|
||||
$component = mountEditComponent($this->user, $voucher);
|
||||
|
||||
expect($component->outlets)->toHaveCount(3);
|
||||
expect($component->outlets)->toHaveKey($outlets->first()->id);
|
||||
});
|
||||
|
||||
it('loads voucher data into form on mount', function () {
|
||||
$outlets = createOutlets(2);
|
||||
$voucher = Voucher::factory()->create([
|
||||
'name' => 'Test Voucher',
|
||||
'code' => 'TEST123',
|
||||
'tags' => 'Promo',
|
||||
'type' => VoucherType::FIXED->value,
|
||||
'discount_amount' => 25000,
|
||||
'min_purchase' => 100000,
|
||||
'max_discount' => null,
|
||||
'quota' => '50',
|
||||
'limit_per_user' => '2',
|
||||
'summary' => 'Test summary',
|
||||
'start_date' => now()->addDay()->format('Y-m-d'),
|
||||
'end_date' => now()->addMonth()->format('Y-m-d'),
|
||||
]);
|
||||
$voucher->outlets()->attach($outlets->pluck('id'));
|
||||
|
||||
$component = mountEditComponent($this->user, $voucher);
|
||||
|
||||
expect($component->form->name)->toBe('Test Voucher');
|
||||
expect($component->form->code)->toBe('TEST123');
|
||||
expect($component->form->tags)->toBe('Promo');
|
||||
expect($component->form->type)->toBe((string) VoucherType::FIXED->value);
|
||||
expect($component->form->summary)->toBe('Test summary');
|
||||
expect($component->form->quota)->toBe('50');
|
||||
expect($component->form->limit_per_user)->toBe('2');
|
||||
expect($component->form->start_date)->toBe(now()->addDay()->format('Y-m-d'));
|
||||
expect($component->form->end_date)->toBe(now()->addMonth()->format('Y-m-d'));
|
||||
expect($component->form->outlet_ids)->toEqual($outlets->pluck('id')->toArray());
|
||||
});
|
||||
|
||||
it('formats currency values correctly when loading voucher', function () {
|
||||
$voucher = Voucher::factory()->create([
|
||||
'discount_amount' => 50000,
|
||||
'min_purchase' => 200000,
|
||||
'max_discount' => 100000,
|
||||
]);
|
||||
$voucher->outlets()->attach(createOutlets(1)->pluck('id'));
|
||||
|
||||
$component = mountEditComponent($this->user, $voucher);
|
||||
|
||||
// Note: parseRupiahToInt returns integer, so we check the raw value
|
||||
// The form will display formatted currency in the view
|
||||
expect($component->form->discount_amount)->toBe('50000');
|
||||
expect($component->form->min_purchase)->toBe('200000');
|
||||
expect($component->form->max_discount)->toBe('100000');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Update Voucher - Fixed Type
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('can update a fixed type voucher', function () {
|
||||
$outlets = createOutlets(3);
|
||||
$voucher = Voucher::factory()->create([
|
||||
'type' => VoucherType::FIXED->value,
|
||||
]);
|
||||
$voucher->outlets()->attach($outlets->take(2)->pluck('id'));
|
||||
|
||||
$newOutlets = createOutlets(2);
|
||||
$allOutlets = $outlets->merge($newOutlets);
|
||||
|
||||
mountEditComponent($this->user, $voucher)
|
||||
->set('form.name', 'Updated Voucher')
|
||||
->set('form.code', 'UPDATED')
|
||||
->set('form.tags', 'Updated Tag')
|
||||
->set('form.discount_amount', '30000')
|
||||
->set('form.min_purchase', '150000')
|
||||
->set('form.quota', '75')
|
||||
->set('form.limit_per_user', '3')
|
||||
->set('form.summary', 'Updated summary')
|
||||
->set('form.start_date', now()->addDays(2)->format('Y-m-d'))
|
||||
->set('form.end_date', now()->addMonths(2)->format('Y-m-d'))
|
||||
->set('form.outlet_ids', $allOutlets->pluck('id')->toArray())
|
||||
->call('save')
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect(route('studio.loyalty.voucher.index'));
|
||||
|
||||
$voucher->refresh();
|
||||
expect($voucher->name)->toBe('Updated Voucher');
|
||||
expect($voucher->code)->toBe('UPDATED');
|
||||
expect($voucher->tags)->toBe('Updated Tag');
|
||||
expect($voucher->discount_amount)->toBe(30000);
|
||||
expect($voucher->min_purchase)->toBe(150000);
|
||||
expect($voucher->quota)->toBe(75);
|
||||
expect($voucher->limit_per_user)->toBe(3);
|
||||
expect($voucher->outlets)->toHaveCount(5);
|
||||
});
|
||||
|
||||
it('can update voucher to remove optional fields', function () {
|
||||
$voucher = Voucher::factory()->create([
|
||||
'min_purchase' => 50000,
|
||||
'quota' => 100,
|
||||
'limit_per_user' => 2,
|
||||
'end_date' => now()->addMonth()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
$voucher->outlets()->attach(createOutlets(1)->pluck('id'));
|
||||
|
||||
mountEditComponent($this->user, $voucher)
|
||||
->set('form.min_purchase', null)
|
||||
->set('form.quota', null)
|
||||
->set('form.limit_per_user', null)
|
||||
->set('form.end_date', null)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$voucher->refresh();
|
||||
expect($voucher->min_purchase)->toBeNull();
|
||||
expect($voucher->quota)->toBeNull();
|
||||
expect($voucher->limit_per_user)->toBeNull();
|
||||
expect($voucher->end_date)->toBeNull();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Update Voucher - Percentage Type
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('can update voucher from fixed to percentage type', function () {
|
||||
$voucher = Voucher::factory()->create([
|
||||
'type' => VoucherType::FIXED->value,
|
||||
'discount_amount' => 10000,
|
||||
'max_discount' => null,
|
||||
]);
|
||||
$voucher->outlets()->attach(createOutlets(1)->pluck('id'));
|
||||
|
||||
mountEditComponent($this->user, $voucher)
|
||||
->set('form.type', VoucherType::PERCENTAGE->value)
|
||||
->set('form.discount_amount', '15')
|
||||
->set('form.max_discount', '40000')
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$voucher->refresh();
|
||||
expect($voucher->type)->toBe(VoucherType::PERCENTAGE);
|
||||
expect($voucher->discount_amount)->toBe(15);
|
||||
expect($voucher->max_discount)->toBe(40000);
|
||||
});
|
||||
|
||||
it('can update voucher from percentage to fixed type', function () {
|
||||
$voucher = Voucher::factory()->create([
|
||||
'type' => VoucherType::PERCENTAGE->value,
|
||||
'discount_amount' => 20,
|
||||
'max_discount' => 50000,
|
||||
]);
|
||||
$voucher->outlets()->attach(createOutlets(1)->pluck('id'));
|
||||
|
||||
mountEditComponent($this->user, $voucher)
|
||||
->set('form.type', VoucherType::FIXED->value)
|
||||
->set('form.discount_amount', '25000')
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$voucher->refresh();
|
||||
expect($voucher->type)->toBe(VoucherType::FIXED);
|
||||
expect($voucher->discount_amount)->toBe(25000);
|
||||
expect($voucher->max_discount)->toBeNull();
|
||||
});
|
||||
|
||||
it('can update percentage voucher with new max discount', function () {
|
||||
$voucher = Voucher::factory()->create([
|
||||
'type' => VoucherType::PERCENTAGE->value,
|
||||
'discount_amount' => 10,
|
||||
'max_discount' => 30000,
|
||||
]);
|
||||
$voucher->outlets()->attach(createOutlets(1)->pluck('id'));
|
||||
|
||||
mountEditComponent($this->user, $voucher)
|
||||
->set('form.discount_amount', '25')
|
||||
->set('form.max_discount', '60000')
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$voucher->refresh();
|
||||
expect($voucher->discount_amount)->toBe(25);
|
||||
expect($voucher->max_discount)->toBe(60000);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Outlet Management
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('can update voucher outlets', function () {
|
||||
$originalOutlets = createOutlets(2);
|
||||
$voucher = Voucher::factory()->create();
|
||||
$voucher->outlets()->attach($originalOutlets->pluck('id'));
|
||||
|
||||
$newOutlets = createOutlets(3);
|
||||
|
||||
mountEditComponent($this->user, $voucher)
|
||||
->set('form.outlet_ids', $newOutlets->pluck('id')->toArray())
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$voucher->refresh();
|
||||
expect($voucher->outlets)->toHaveCount(3);
|
||||
expect($voucher->outlets->pluck('id')->toArray())->toEqual($newOutlets->pluck('id')->toArray());
|
||||
});
|
||||
|
||||
it('can add more outlets to existing voucher', function () {
|
||||
$originalOutlets = createOutlets(2);
|
||||
$voucher = Voucher::factory()->create();
|
||||
$voucher->outlets()->attach($originalOutlets->pluck('id'));
|
||||
|
||||
$additionalOutlets = createOutlets(2);
|
||||
$allOutlets = $originalOutlets->merge($additionalOutlets);
|
||||
|
||||
mountEditComponent($this->user, $voucher)
|
||||
->set('form.outlet_ids', $allOutlets->pluck('id')->toArray())
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$voucher->refresh();
|
||||
expect($voucher->outlets)->toHaveCount(4);
|
||||
});
|
||||
|
||||
it('can remove all outlets from voucher', function () {
|
||||
$outlets = createOutlets(3);
|
||||
$voucher = Voucher::factory()->create();
|
||||
$voucher->outlets()->attach($outlets->pluck('id'));
|
||||
|
||||
mountEditComponent($this->user, $voucher)
|
||||
->set('form.outlet_ids', [])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.outlet_ids']); // Should fail validation
|
||||
});
|
||||
|
||||
it('can select all outlets when editing', function () {
|
||||
$outlets = createOutlets(5);
|
||||
$voucher = Voucher::factory()->create();
|
||||
$voucher->outlets()->attach($outlets->take(2)->pluck('id'));
|
||||
|
||||
$component = mountEditComponent($this->user, $voucher);
|
||||
$component->call('selectAllOutlets');
|
||||
|
||||
expect($component->form->outlet_ids)->toHaveCount(5);
|
||||
expect($component->form->outlet_ids)->toEqual($outlets->pluck('id')->toArray());
|
||||
});
|
||||
|
||||
it('can deselect all outlets when editing', function () {
|
||||
$outlets = createOutlets(3);
|
||||
$voucher = Voucher::factory()->create();
|
||||
$voucher->outlets()->attach($outlets->pluck('id'));
|
||||
|
||||
$component = mountEditComponent($this->user, $voucher);
|
||||
expect($component->form->outlet_ids)->toHaveCount(3);
|
||||
|
||||
$component->call('deselectAllOutlets');
|
||||
|
||||
expect($component->form->outlet_ids)->toHaveCount(0);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Validation Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates required fields on update', function () {
|
||||
$voucher = Voucher::factory()->create();
|
||||
$voucher->outlets()->attach(createOutlets(1)->pluck('id'));
|
||||
|
||||
mountEditComponent($this->user, $voucher)
|
||||
->set('form.name', '')
|
||||
->set('form.code', '')
|
||||
->set('form.tags', '')
|
||||
->set('form.summary', '')
|
||||
->set('form.start_date', '')
|
||||
->set('form.outlet_ids', [])
|
||||
->call('save')
|
||||
->assertHasErrors([
|
||||
'form.name',
|
||||
'form.code',
|
||||
'form.tags',
|
||||
'form.summary',
|
||||
'form.start_date',
|
||||
'form.outlet_ids',
|
||||
]);
|
||||
});
|
||||
|
||||
it('allows same code when updating voucher', function () {
|
||||
$voucher = Voucher::factory()->create(['code' => 'EXISTING']);
|
||||
$voucher->outlets()->attach(createOutlets(1)->pluck('id'));
|
||||
|
||||
mountEditComponent($this->user, $voucher)
|
||||
->set('form.name', 'Updated Name')
|
||||
->set('form.code', 'EXISTING') // Same code
|
||||
->set('form.tags', 'Test')
|
||||
->set('form.type', VoucherType::FIXED->value)
|
||||
->set('form.discount_amount', '10000')
|
||||
->set('form.summary', 'Test')
|
||||
->set('form.start_date', now()->addDay()->format('Y-m-d'))
|
||||
->set('form.outlet_ids', [$voucher->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
|
||||
it('validates code uniqueness when changing to existing code', function () {
|
||||
$voucher1 = Voucher::factory()->create(['code' => 'VOUCHER1']);
|
||||
$voucher2 = Voucher::factory()->create(['code' => 'VOUCHER2']);
|
||||
$voucher1->outlets()->attach(createOutlets(1)->pluck('id'));
|
||||
|
||||
mountEditComponent($this->user, $voucher1)
|
||||
->set('form.code', 'VOUCHER2') // Trying to use existing code
|
||||
->set('form.name', 'Test')
|
||||
->set('form.tags', 'Test')
|
||||
->set('form.type', VoucherType::FIXED->value)
|
||||
->set('form.discount_amount', '10000')
|
||||
->set('form.summary', 'Test')
|
||||
->set('form.start_date', now()->addDay()->format('Y-m-d'))
|
||||
->set('form.outlet_ids', [$voucher1->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.code']);
|
||||
});
|
||||
|
||||
it('validates percentage discount requires max_discount on update', function () {
|
||||
$voucher = Voucher::factory()->create([
|
||||
'type' => VoucherType::PERCENTAGE->value,
|
||||
'max_discount' => 50000,
|
||||
]);
|
||||
$voucher->outlets()->attach(createOutlets(1)->pluck('id'));
|
||||
|
||||
mountEditComponent($this->user, $voucher)
|
||||
->set('form.max_discount', null)
|
||||
->call('save')
|
||||
->assertHasErrors(['form.max_discount']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Permissions
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('prevents update when user lacks update voucher permission', function () {
|
||||
$this->ownerRole->revokePermissionTo('update voucher');
|
||||
Gate::define('update voucher', fn () => false);
|
||||
|
||||
$voucher = Voucher::factory()->create();
|
||||
$voucher->outlets()->attach(createOutlets(1)->pluck('id'));
|
||||
|
||||
mountEditComponent($this->user, $voucher)
|
||||
->set('form.name', 'Updated')
|
||||
->set('form.code', 'UPDATED')
|
||||
->set('form.tags', 'Test')
|
||||
->set('form.type', VoucherType::FIXED->value)
|
||||
->set('form.discount_amount', '10000')
|
||||
->set('form.summary', 'Test')
|
||||
->set('form.start_date', now()->addDay()->format('Y-m-d'))
|
||||
->set('form.outlet_ids', [$voucher->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('allows update when user has update voucher permission', function () {
|
||||
$voucher = Voucher::factory()->create();
|
||||
$voucher->outlets()->attach(createOutlets(1)->pluck('id'));
|
||||
|
||||
mountEditComponent($this->user, $voucher)
|
||||
->set('form.name', 'Updated')
|
||||
->set('form.code', 'UPDATED')
|
||||
->set('form.tags', 'Test')
|
||||
->set('form.type', VoucherType::FIXED->value)
|
||||
->set('form.discount_amount', '10000')
|
||||
->set('form.summary', 'Test')
|
||||
->set('form.start_date', now()->addDay()->format('Y-m-d'))
|
||||
->set('form.outlet_ids', [$voucher->outlets->first()->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Edge Cases
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('handles voucher with no outlets initially', function () {
|
||||
$voucher = Voucher::factory()->create();
|
||||
// No outlets attached
|
||||
|
||||
$outlets = createOutlets(2);
|
||||
|
||||
mountEditComponent($this->user, $voucher)
|
||||
->set('form.name', 'Test')
|
||||
->set('form.code', 'TEST')
|
||||
->set('form.tags', 'Test')
|
||||
->set('form.type', VoucherType::FIXED->value)
|
||||
->set('form.discount_amount', '10000')
|
||||
->set('form.summary', 'Test')
|
||||
->set('form.start_date', now()->addDay()->format('Y-m-d'))
|
||||
->set('form.outlet_ids', $outlets->pluck('id')->toArray())
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$voucher->refresh();
|
||||
expect($voucher->outlets)->toHaveCount(2);
|
||||
});
|
||||
|
||||
it('preserves available_count when updating quota', function () {
|
||||
$voucher = Voucher::factory()->create([
|
||||
'quota' => 100,
|
||||
'available_count' => 50, // Some vouchers already used
|
||||
]);
|
||||
$voucher->outlets()->attach(createOutlets(1)->pluck('id'));
|
||||
|
||||
// Note: The form doesn't update available_count, only quota
|
||||
// This test verifies that available_count is not reset
|
||||
mountEditComponent($this->user, $voucher)
|
||||
->set('form.quota', '200')
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$voucher->refresh();
|
||||
|
||||
expect($voucher->quota)->toBe(200);
|
||||
// available_count should remain 50 (not reset to 200)
|
||||
expect($voucher->available_count)->toBe(50);
|
||||
});
|
||||
@ -1,312 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Studio\Loyalty\Voucher\Index;
|
||||
use App\Models\Employee;
|
||||
use App\Models\User;
|
||||
use App\Models\Voucher;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
|
||||
$this->ownerRole = Role::where('name', 'Owner')->first();
|
||||
|
||||
$this->user->roles()->attach($this->ownerRole->id);
|
||||
});
|
||||
|
||||
function mountIndexComponent(User $user)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(Index::class);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders page successfully', function () {
|
||||
mountIndexComponent($this->user)
|
||||
->assertViewIs('livewire.studio.loyalty.voucher.index')
|
||||
->assertViewHas('pageTitle', 'Voucher');
|
||||
});
|
||||
|
||||
it('displays all vouchers', function () {
|
||||
Voucher::factory()->count(10)->create();
|
||||
|
||||
$voucher = Voucher::first();
|
||||
|
||||
mountIndexComponent($this->user)
|
||||
->assertSee($voucher->name);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Statistics
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('displays correct total voucher count', function () {
|
||||
Voucher::factory()->count(5)->create();
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect($component->stats)->toHaveCount(4);
|
||||
expect($component->stats[0]['title'])->toBe('Total Voucher');
|
||||
expect($component->stats[0]['value'])->toBe(5);
|
||||
});
|
||||
|
||||
it('displays correct active voucher count', function () {
|
||||
// Active vouchers: start_date <= now AND (end_date is null OR end_date >= now)
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->subDay()->format('Y-m-d'),
|
||||
'end_date' => now()->addDay()->format('Y-m-d'),
|
||||
]);
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->subDay()->format('Y-m-d'),
|
||||
'end_date' => null,
|
||||
]);
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->subWeek()->format('Y-m-d'),
|
||||
'end_date' => now()->addWeek()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
// Inactive vouchers
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->subWeek()->format('Y-m-d'),
|
||||
'end_date' => now()->subDay()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
// Upcoming vouchers
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->addDay()->format('Y-m-d'),
|
||||
'end_date' => now()->addWeek()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect($component->stats[1]['title'])->toBe('Voucher Aktif');
|
||||
expect($component->stats[1]['value'])->toBe(3);
|
||||
});
|
||||
|
||||
it('displays correct inactive voucher count', function () {
|
||||
// Inactive vouchers: end_date < now
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->subWeek()->format('Y-m-d'),
|
||||
'end_date' => now()->subDay()->format('Y-m-d'),
|
||||
]);
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->subMonth()->format('Y-m-d'),
|
||||
'end_date' => now()->subWeek()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
// Active vouchers
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->subDay()->format('Y-m-d'),
|
||||
'end_date' => now()->addDay()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
// Upcoming vouchers
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->addDay()->format('Y-m-d'),
|
||||
'end_date' => now()->addWeek()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect($component->stats[2]['title'])->toBe('Voucher Tidak Aktif');
|
||||
expect($component->stats[2]['value'])->toBe(2);
|
||||
});
|
||||
|
||||
it('displays correct upcoming voucher count', function () {
|
||||
// Upcoming vouchers: start_date > now
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->addDay()->format('Y-m-d'),
|
||||
'end_date' => now()->addWeek()->format('Y-m-d'),
|
||||
]);
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->addWeek()->format('Y-m-d'),
|
||||
'end_date' => now()->addMonth()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
// Active vouchers
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->subDay()->format('Y-m-d'),
|
||||
'end_date' => now()->addDay()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
// Inactive vouchers
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->subWeek()->format('Y-m-d'),
|
||||
'end_date' => now()->subDay()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect($component->stats[3]['title'])->toBe('Voucher Akan Datang');
|
||||
expect($component->stats[3]['value'])->toBe(2);
|
||||
});
|
||||
|
||||
it('calculates stats correctly with mixed voucher statuses', function () {
|
||||
// Create vouchers with different statuses
|
||||
Voucher::factory()->count(3)->create([
|
||||
'start_date' => now()->subDay()->format('Y-m-d'),
|
||||
'end_date' => now()->addDay()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
Voucher::factory()->count(2)->create([
|
||||
'start_date' => now()->subWeek()->format('Y-m-d'),
|
||||
'end_date' => now()->subDay()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
Voucher::factory()->count(4)->create([
|
||||
'start_date' => now()->addDay()->format('Y-m-d'),
|
||||
'end_date' => now()->addWeek()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect($component->stats[0]['value'])->toBe(9); // Total
|
||||
expect($component->stats[1]['value'])->toBe(3); // Active
|
||||
expect($component->stats[2]['value'])->toBe(2); // Inactive
|
||||
expect($component->stats[3]['value'])->toBe(4); // Upcoming
|
||||
});
|
||||
|
||||
it('handles vouchers with null end_date in stats', function () {
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->subDay()->format('Y-m-d'),
|
||||
'end_date' => null, // Never expires
|
||||
]);
|
||||
|
||||
Voucher::factory()->create([
|
||||
'start_date' => now()->addDay()->format('Y-m-d'),
|
||||
'end_date' => null, // Upcoming, never expires
|
||||
]);
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect($component->stats[1]['value'])->toBe(1); // Active (started, no end date)
|
||||
expect($component->stats[3]['value'])->toBe(1); // Upcoming (not started yet)
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Delete Voucher
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('can delete a voucher', function () {
|
||||
$voucher = Voucher::factory()->create();
|
||||
|
||||
mountIndexComponent($this->user)
|
||||
->call('delete', $voucher)
|
||||
->assertHasNoErrors()
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
expect(Voucher::count())->toBe(0);
|
||||
$this->assertSoftDeleted('vouchers', ['id' => $voucher->id]);
|
||||
});
|
||||
|
||||
it('can delete multiple vouchers', function () {
|
||||
$vouchers = Voucher::factory()->count(5)->create();
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
foreach ($vouchers as $voucher) {
|
||||
$component->call('delete', $voucher);
|
||||
}
|
||||
|
||||
expect(Voucher::count())->toBe(0);
|
||||
});
|
||||
|
||||
it('soft deletes voucher when deleting', function () {
|
||||
$voucher = Voucher::factory()->create();
|
||||
|
||||
mountIndexComponent($this->user)
|
||||
->call('delete', $voucher);
|
||||
|
||||
expect(Voucher::withTrashed()->count())->toBe(1);
|
||||
expect(Voucher::count())->toBe(0);
|
||||
expect($voucher->fresh()->trashed())->toBeTrue();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Permissions
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('displays create button when user has create voucher permission', function () {
|
||||
mountIndexComponent($this->user)
|
||||
->assertSee('Tambah');
|
||||
});
|
||||
|
||||
it('hides create button when user lacks create voucher permission', function () {
|
||||
$this->ownerRole->revokePermissionTo('create voucher');
|
||||
Gate::define('create voucher', fn () => false);
|
||||
|
||||
mountIndexComponent($this->user)
|
||||
->assertDontSee('Tambah');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Edge Cases
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('handles empty voucher list', function () {
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect($component->stats[0]['value'])->toBe(0);
|
||||
expect($component->stats[1]['value'])->toBe(0);
|
||||
expect($component->stats[2]['value'])->toBe(0);
|
||||
expect($component->stats[3]['value'])->toBe(0);
|
||||
});
|
||||
|
||||
it('handles voucher with same start and end date', function () {
|
||||
$today = now()->format('Y-m-d');
|
||||
|
||||
Voucher::factory()->create([
|
||||
'start_date' => $today,
|
||||
'end_date' => $today,
|
||||
]);
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
// Should be counted as active (start_date <= now AND end_date >= now)
|
||||
expect($component->stats[1]['value'])->toBe(1);
|
||||
});
|
||||
|
||||
it('updates stats when vouchers are deleted', function () {
|
||||
$voucher1 = Voucher::factory()->create([
|
||||
'start_date' => now()->subDay()->format('Y-m-d'),
|
||||
'end_date' => now()->addDay()->format('Y-m-d'),
|
||||
]);
|
||||
$voucher2 = Voucher::factory()->create([
|
||||
'start_date' => now()->addDay()->format('Y-m-d'),
|
||||
'end_date' => now()->addWeek()->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
expect($component->stats[0]['value'])->toBe(2);
|
||||
expect($component->stats[1]['value'])->toBe(1);
|
||||
expect($component->stats[3]['value'])->toBe(1);
|
||||
|
||||
$component->call('delete', $voucher1);
|
||||
|
||||
// Note: Stats are calculated in mount(), so they won't update automatically
|
||||
// This test verifies the delete works, but stats would need remount to update
|
||||
expect(Voucher::count())->toBe(1);
|
||||
});
|
||||
@ -1,136 +0,0 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__.'/TestHelpers.php';
|
||||
|
||||
use App\Livewire\Studio\Manage\Article\Create;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(\App\Models\Employee::factory())
|
||||
->create();
|
||||
|
||||
$this->ownerRole = Role::where('name', 'Owner')->first();
|
||||
$this->user->roles()->attach($this->ownerRole->id);
|
||||
|
||||
Queue::fake();
|
||||
});
|
||||
|
||||
function mountCreateComponent(User $user): \Livewire\Features\SupportTesting\Testable
|
||||
{
|
||||
return Livewire::actingAs($user)->test(Create::class);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders page successfully', function () {
|
||||
mountCreateComponent($this->user)
|
||||
->assertViewIs('livewire.studio.manage.article.form')
|
||||
->assertViewHas('pageTitle', 'Tambah Artikel');
|
||||
});
|
||||
|
||||
it('mounts with form initialized', function () {
|
||||
$component = mountCreateComponent($this->user);
|
||||
|
||||
expect($component->form)->toBeInstanceOf(\App\Livewire\Forms\Studio\Manage\ArticleForm::class);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authorization Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('requires create article permission', function () {
|
||||
// Temporarily remove the permission from the user
|
||||
$this->ownerRole->revokePermissionTo('create article');
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->call('save')
|
||||
->assertForbidden();
|
||||
|
||||
// Restore the permission
|
||||
$this->user->givePermissionTo('create article');
|
||||
});
|
||||
|
||||
it('allows creation with proper permissions', function () {
|
||||
mountCreateComponent($this->user)
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Form Validation
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates form data before saving', function () {
|
||||
mountCreateComponent($this->user)
|
||||
->call('save')
|
||||
->assertHasErrors([
|
||||
'form.title' => 'required',
|
||||
'form.excerpt' => 'required',
|
||||
'form.content' => 'required',
|
||||
'form.thumbnail' => 'required',
|
||||
]);
|
||||
});
|
||||
|
||||
// Validation is already tested in 'it validates form data before saving'
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Article Creation
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// Article creation is tested functionally through the save method
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Component Structure
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('has form property', function () {
|
||||
$component = mountCreateComponent($this->user);
|
||||
|
||||
expect($component->form)->toBeInstanceOf(\App\Livewire\Forms\Studio\Manage\ArticleForm::class);
|
||||
});
|
||||
|
||||
it('has save method', function () {
|
||||
$component = new Create;
|
||||
|
||||
expect(method_exists($component, 'save'))->toBeTrue();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Component Title
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('has title attribute set', function () {
|
||||
$reflection = new ReflectionClass(Create::class);
|
||||
$attributes = $reflection->getAttributes();
|
||||
|
||||
$titleAttribute = collect($attributes)->first(function ($attribute) {
|
||||
return $attribute->getName() === 'Livewire\\Attributes\\Title';
|
||||
});
|
||||
|
||||
expect($titleAttribute)->not->toBeNull();
|
||||
expect($titleAttribute->getArguments()[0])->toBe('Tambah Artikel');
|
||||
});
|
||||
@ -1,177 +0,0 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__.'/TestHelpers.php';
|
||||
|
||||
use App\Enums\ArticleStatus;
|
||||
use App\Livewire\Studio\Manage\Article\Edit;
|
||||
use App\Models\Article;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(\App\Models\Employee::factory())
|
||||
->create();
|
||||
|
||||
$this->ownerRole = Role::where('name', 'Owner')->first();
|
||||
$this->user->roles()->attach($this->ownerRole->id);
|
||||
|
||||
$this->article = Article::factory()->create([
|
||||
'author_id' => $this->user->id,
|
||||
'status' => ArticleStatus::DRAFT,
|
||||
]);
|
||||
});
|
||||
|
||||
function mountEditComponent(User $user, Article $article): \Livewire\Features\SupportTesting\Testable
|
||||
{
|
||||
return Livewire::actingAs($user)->test(Edit::class, ['article' => $article->hash]);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders page successfully', function () {
|
||||
mountEditComponent($this->user, $this->article)
|
||||
->assertViewIs('livewire.studio.manage.article.form')
|
||||
->assertViewHas('pageTitle', 'Ubah Artikel');
|
||||
});
|
||||
|
||||
it('mounts with article data loaded', function () {
|
||||
$component = mountEditComponent($this->user, $this->article);
|
||||
|
||||
expect($component->form)->toBeInstanceOf(\App\Livewire\Forms\Studio\Manage\ArticleForm::class);
|
||||
expect($component->form->article)->toBeInstanceOf(Article::class);
|
||||
expect($component->form->title)->toBe($this->article->title);
|
||||
expect($component->form->excerpt)->toBe($this->article->excerpt);
|
||||
expect($component->form->content)->toBe($this->article->content);
|
||||
expect($component->form->status)->toBe((string) $this->article->status->value);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authorization Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('requires update article permission', function () {
|
||||
// Temporarily remove the permission from the user
|
||||
$this->ownerRole->revokePermissionTo('update article');
|
||||
|
||||
mountEditComponent($this->user, $this->article)
|
||||
->call('save')
|
||||
->assertForbidden();
|
||||
|
||||
// Restore the permission
|
||||
$this->user->givePermissionTo('update article');
|
||||
});
|
||||
|
||||
it('allows editing with proper permissions', function () {
|
||||
mountEditComponent($this->user, $this->article)
|
||||
->assertOk();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Form Validation
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('validates form data before saving', function () {
|
||||
mountEditComponent($this->user, $this->article)
|
||||
->set('form.title', '')
|
||||
->set('form.excerpt', '')
|
||||
->set('form.content', '')
|
||||
->set('form.thumbnail', [])
|
||||
->call('save')
|
||||
->assertHasErrors([
|
||||
'form.title' => 'required',
|
||||
'form.excerpt' => 'required',
|
||||
'form.content' => 'required',
|
||||
'form.thumbnail' => 'required',
|
||||
]);
|
||||
});
|
||||
|
||||
it('validates title field', function () {
|
||||
mountEditComponent($this->user, $this->article)
|
||||
->set('form.title', '')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.title' => 'required']);
|
||||
});
|
||||
|
||||
it('validates excerpt field', function () {
|
||||
mountEditComponent($this->user, $this->article)
|
||||
->set('form.excerpt', '')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.excerpt' => 'required']);
|
||||
});
|
||||
|
||||
it('validates content field', function () {
|
||||
mountEditComponent($this->user, $this->article)
|
||||
->set('form.content', '')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.content' => 'required']);
|
||||
});
|
||||
|
||||
it('validates status field', function () {
|
||||
mountEditComponent($this->user, $this->article)
|
||||
->set('form.status', 'invalid_status')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.status']);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Article Update
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// Article updates and events are tested functionally through the save method
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Component Title
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('has title attribute set', function () {
|
||||
$reflection = new ReflectionClass(Edit::class);
|
||||
$attributes = $reflection->getAttributes();
|
||||
|
||||
$titleAttribute = collect($attributes)->first(function ($attribute) {
|
||||
return $attribute->getName() === 'Livewire\\Attributes\\Title';
|
||||
});
|
||||
|
||||
expect($titleAttribute)->not->toBeNull();
|
||||
expect($titleAttribute->getArguments()[0])->toBe('Ubah Artikel');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Mount Method
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// Mount method is tested through component mounting in other tests
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Error Handling
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('handles non-existent article gracefully', function () {
|
||||
// This would typically throw an exception or redirect
|
||||
// For now, we just verify the component can be instantiated
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
@ -1,245 +0,0 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__.'/TestHelpers.php';
|
||||
|
||||
use App\Enums\ArticleStatus;
|
||||
use App\Livewire\Studio\Manage\Article\Index;
|
||||
use App\Models\Article;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(\App\Models\Employee::factory())
|
||||
->create();
|
||||
|
||||
$this->ownerRole = Role::where('name', 'Owner')->first();
|
||||
$this->user->roles()->attach($this->ownerRole->id);
|
||||
|
||||
$this->article = Article::factory()->create([
|
||||
'author_id' => $this->user->id,
|
||||
'status' => ArticleStatus::PUBLISHED,
|
||||
]);
|
||||
});
|
||||
|
||||
function mountIndexComponent(User $user): \Livewire\Features\SupportTesting\Testable
|
||||
{
|
||||
return Livewire::actingAs($user)->test(Index::class);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders page successfully', function () {
|
||||
mountIndexComponent($this->user)
|
||||
->assertViewIs('livewire.studio.manage.article.index')
|
||||
->assertViewHas('pageTitle', 'Artikel');
|
||||
});
|
||||
|
||||
it('mounts successfully', function () {
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect($component->instance())->toBeInstanceOf(Index::class);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Component Properties
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('has title attribute set', function () {
|
||||
$reflection = new ReflectionClass(Index::class);
|
||||
$attributes = $reflection->getAttributes();
|
||||
|
||||
$titleAttribute = collect($attributes)->first(function ($attribute) {
|
||||
return $attribute->getName() === 'Livewire\\Attributes\\Title';
|
||||
});
|
||||
|
||||
expect($titleAttribute)->not->toBeNull();
|
||||
expect($titleAttribute->getArguments()[0])->toBe('Artikel');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Delete Functionality
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('deletes article successfully', function () {
|
||||
$articleToDelete = Article::factory()->create([
|
||||
'author_id' => $this->user->id,
|
||||
'status' => ArticleStatus::DRAFT,
|
||||
]);
|
||||
|
||||
mountIndexComponent($this->user)
|
||||
->call('delete', $articleToDelete)
|
||||
->assertDispatched('refreshDatatable');
|
||||
|
||||
// Verify article is soft deleted
|
||||
expect($articleToDelete->fresh()->trashed())->toBeTrue();
|
||||
});
|
||||
|
||||
it('soft deletes article', function () {
|
||||
$articleToDelete = Article::factory()->create([
|
||||
'author_id' => $this->user->id,
|
||||
'status' => ArticleStatus::DRAFT,
|
||||
]);
|
||||
|
||||
mountIndexComponent($this->user)
|
||||
->call('delete', $articleToDelete);
|
||||
|
||||
// Check if article exists with trashed
|
||||
$deletedArticle = Article::withTrashed()->find($articleToDelete->id);
|
||||
expect($deletedArticle)->not->toBeNull();
|
||||
expect($deletedArticle->trashed())->toBeTrue();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Delete Authorization
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('allows deletion with delete permission', function () {
|
||||
$articleToDelete = Article::factory()->create([
|
||||
'author_id' => $this->user->id,
|
||||
'status' => ArticleStatus::DRAFT,
|
||||
]);
|
||||
|
||||
mountIndexComponent($this->user)
|
||||
->call('delete', $articleToDelete);
|
||||
|
||||
expect(Article::withTrashed()->find($articleToDelete->id)->trashed())->toBeTrue();
|
||||
});
|
||||
|
||||
it('handles deletion of non-existent article gracefully', function () {
|
||||
$nonExistentArticle = new Article;
|
||||
$nonExistentArticle->id = 99999;
|
||||
|
||||
mountIndexComponent($this->user)
|
||||
->call('delete', $nonExistentArticle)
|
||||
->assertDispatched('refreshDatatable');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Component Events
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('dispatches refreshDatatable event on delete', function () {
|
||||
$articleToDelete = Article::factory()->create([
|
||||
'author_id' => $this->user->id,
|
||||
'status' => ArticleStatus::DRAFT,
|
||||
]);
|
||||
|
||||
mountIndexComponent($this->user)
|
||||
->call('delete', $articleToDelete)
|
||||
->assertDispatched('refreshDatatable');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Toast Messages
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
// Toast messages are tested functionally through the delete method
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Flux Modal Handling
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('closes flux modal after deletion', function () {
|
||||
$articleToDelete = Article::factory()->create([
|
||||
'author_id' => $this->user->id,
|
||||
'status' => ArticleStatus::DRAFT,
|
||||
]);
|
||||
|
||||
mountIndexComponent($this->user)
|
||||
->call('delete', $articleToDelete);
|
||||
|
||||
// This would typically test Flux modal closure
|
||||
// For now, we verify the method completes
|
||||
expect(true)->toBeTrue();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Traits Usage
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('uses WithConfirmation trait', function () {
|
||||
$component = new Index;
|
||||
|
||||
expect(in_array('App\Traits\WithConfirmation', class_uses_recursive($component)))->toBeTrue();
|
||||
});
|
||||
|
||||
it('uses WithSubscribeNotification trait', function () {
|
||||
$component = new Index;
|
||||
|
||||
expect(in_array('App\Traits\Notification\WithSubscribeNotification', class_uses_recursive($component)))->toBeTrue();
|
||||
});
|
||||
|
||||
it('uses WithToast trait', function () {
|
||||
$component = new Index;
|
||||
|
||||
expect(in_array('App\Traits\WithToast', class_uses_recursive($component)))->toBeTrue();
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Render Method
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('has render method', function () {
|
||||
$component = new Index;
|
||||
|
||||
expect(method_exists($component, 'render'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('render method returns correct view', function () {
|
||||
$component = new Index;
|
||||
|
||||
$view = $component->render();
|
||||
|
||||
expect($view->getName())->toBe('livewire.studio.manage.article.index');
|
||||
expect($view->getData()['pageTitle'])->toBe('Artikel');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Component Structure
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('has delete method', function () {
|
||||
$component = new Index;
|
||||
|
||||
expect(method_exists($component, 'delete'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('delete method requires article parameter', function () {
|
||||
$component = new Index;
|
||||
$reflection = new ReflectionMethod($component, 'delete');
|
||||
$parameters = $reflection->getParameters();
|
||||
|
||||
expect(count($parameters))->toBe(1);
|
||||
expect($parameters[0]->getType()->getName())->toBe(Article::class);
|
||||
});
|
||||
@ -1,4 +0,0 @@
|
||||
<?php
|
||||
|
||||
// Helper functions for Article tests
|
||||
// Add helper functions here if needed in the future
|
||||
@ -1,198 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Studio\Master\Formula;
|
||||
use App\Models\Bottle;
|
||||
use App\Models\Formula as FormulaModel;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
// Setup Permissions
|
||||
Permission::create(['name' => 'create formula']);
|
||||
Permission::create(['name' => 'update formula']);
|
||||
Permission::create(['name' => 'delete formula']);
|
||||
});
|
||||
|
||||
function createAuthorizedUser()
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->givePermissionTo(['create formula', 'update formula', 'delete formula']);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
it('renders successfully', function () {
|
||||
$user = createAuthorizedUser();
|
||||
$this->actingAs($user);
|
||||
|
||||
Bottle::factory()->create(['size' => 100]);
|
||||
|
||||
Livewire::test(Formula::class)
|
||||
->assertStatus(200);
|
||||
});
|
||||
|
||||
it('validates required fields', function () {
|
||||
$user = createAuthorizedUser();
|
||||
$this->actingAs($user);
|
||||
|
||||
Livewire::test(Formula::class)
|
||||
->set('form.quality', '')
|
||||
->set('form.size', '')
|
||||
->set('form.volume', '')
|
||||
->call('create')
|
||||
->assertHasErrors(['form.quality', 'form.size', 'form.volume']);
|
||||
});
|
||||
|
||||
it('validates quality max length', function () {
|
||||
$user = createAuthorizedUser();
|
||||
$this->actingAs($user);
|
||||
|
||||
Livewire::test(Formula::class)
|
||||
->set('form.quality', str_repeat('a', 21))
|
||||
->call('create')
|
||||
->assertHasErrors(['form.quality']);
|
||||
});
|
||||
|
||||
it('validates size numeric and min', function () {
|
||||
$user = createAuthorizedUser();
|
||||
$this->actingAs($user);
|
||||
|
||||
Livewire::test(Formula::class)
|
||||
->set('form.size', 'abc')
|
||||
->call('create')
|
||||
->assertHasErrors(['form.size']);
|
||||
|
||||
Livewire::test(Formula::class)
|
||||
->set('form.size', 0)
|
||||
->call('create')
|
||||
->assertHasErrors(['form.size']);
|
||||
});
|
||||
|
||||
it('can create a formula', function () {
|
||||
$user = createAuthorizedUser();
|
||||
$this->actingAs($user);
|
||||
|
||||
Bottle::factory()->create(['size' => 100]);
|
||||
|
||||
Livewire::test(Formula::class)
|
||||
->set('form.quality', 'Premium')
|
||||
->set('form.size', '100')
|
||||
->set('form.volume', '50')
|
||||
->call('create');
|
||||
|
||||
$this->assertDatabaseHas('formulas', [
|
||||
'quality' => 'Premium',
|
||||
'size' => 100,
|
||||
'volume' => 50,
|
||||
]);
|
||||
});
|
||||
|
||||
it('prevents duplicate formula creation', function () {
|
||||
$user = createAuthorizedUser();
|
||||
$this->actingAs($user);
|
||||
|
||||
FormulaModel::create([
|
||||
'quality' => 'Premium',
|
||||
'size' => 100,
|
||||
'volume' => 50,
|
||||
]);
|
||||
|
||||
Livewire::test(Formula::class)
|
||||
->set('form.quality', 'Premium')
|
||||
->set('form.size', '100')
|
||||
->set('form.volume', '60')
|
||||
->call('create');
|
||||
|
||||
expect(FormulaModel::count())->toBe(1);
|
||||
});
|
||||
|
||||
it('can update a formula', function () {
|
||||
$user = createAuthorizedUser();
|
||||
$this->actingAs($user);
|
||||
|
||||
$formula = FormulaModel::create([
|
||||
'quality' => 'Premium',
|
||||
'size' => 100,
|
||||
'volume' => 50,
|
||||
]);
|
||||
|
||||
Livewire::test(Formula::class)
|
||||
->call('openModal', 'update', 'Edit Formula', $formula->hash)
|
||||
->assertSet('form.quality', 'Premium')
|
||||
->assertSet('form.size', 100)
|
||||
->set('form.quality', 'Super Premium')
|
||||
->set('form.volume', '55')
|
||||
->call('update');
|
||||
|
||||
$this->assertDatabaseHas('formulas', [
|
||||
'id' => $formula->id,
|
||||
'quality' => 'Super Premium',
|
||||
'size' => 100,
|
||||
'volume' => 55,
|
||||
]);
|
||||
});
|
||||
|
||||
it('prevents duplicate formula on update', function () {
|
||||
$user = createAuthorizedUser();
|
||||
$this->actingAs($user);
|
||||
|
||||
$formula1 = FormulaModel::create(['quality' => 'Premium', 'size' => 100, 'volume' => 50]);
|
||||
$formula2 = FormulaModel::create(['quality' => 'Super Premium', 'size' => 100, 'volume' => 60]);
|
||||
|
||||
Livewire::test(Formula::class)
|
||||
->call('openModal', 'update', 'Edit Formula', $formula1->hash)
|
||||
->set('form.quality', 'Super Premium')
|
||||
->set('form.size', '100')
|
||||
->call('update');
|
||||
|
||||
$formula1->refresh();
|
||||
expect($formula1->quality)->toBe('Premium');
|
||||
});
|
||||
|
||||
it('can delete a formula', function () {
|
||||
$user = createAuthorizedUser();
|
||||
$this->actingAs($user);
|
||||
|
||||
$formula = FormulaModel::create(['quality' => 'Premium', 'size' => 100, 'volume' => 50]);
|
||||
|
||||
Livewire::test(Formula::class)
|
||||
->call('delete', $formula->hash);
|
||||
|
||||
$this->assertSoftDeleted('formulas', ['id' => $formula->id]);
|
||||
});
|
||||
|
||||
it('checks authorization for create', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
Livewire::test(Formula::class)
|
||||
->call('create')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('checks authorization for update', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$formula = FormulaModel::create(['quality' => 'Premium', 'size' => 100, 'volume' => 50]);
|
||||
|
||||
Livewire::test(Formula::class)
|
||||
->call('openModal', 'update', 'Edit', $formula->hash)
|
||||
->call('update')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('checks authorization for delete', function () {
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$formula = FormulaModel::create(['quality' => 'Premium', 'size' => 100, 'volume' => 50]);
|
||||
|
||||
Livewire::test(Formula::class)
|
||||
->call('delete', $formula->hash)
|
||||
->assertForbidden();
|
||||
});
|
||||
@ -1,425 +0,0 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__.'/TestHelpers.php';
|
||||
|
||||
use App\Enums\Day;
|
||||
use App\Enums\OutletStatus;
|
||||
use App\Livewire\Studio\Master\Outlet\Create;
|
||||
use App\Livewire\Studio\Master\Outlet\Index;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Facility;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
});
|
||||
|
||||
function mountCreateComponent(User $user)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(Create::class);
|
||||
}
|
||||
|
||||
it('renders page successfully', function () {
|
||||
mountCreateComponent($this->user)
|
||||
->assertViewIs('livewire.studio.master.outlet.form')
|
||||
->assertViewHas('pageTitle', 'Tambah Outlet');
|
||||
});
|
||||
|
||||
it('mounts days and form opening_hours correctly', function () {
|
||||
$component = mountCreateComponent($this->user);
|
||||
$days = Day::cases();
|
||||
|
||||
expect($component->days)->toHaveCount(count($days));
|
||||
|
||||
foreach ($days as $day) {
|
||||
expect(array_key_exists($day->value, $component->form->opening_hours))->toBeTrue();
|
||||
expect($component->form->opening_hours[$day->value])->toBe(['open_time' => null, 'close_time' => null]);
|
||||
}
|
||||
});
|
||||
|
||||
it('prevents create when user is unauthorized', function () {
|
||||
Gate::define('create outlet', fn () => false);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->call('save')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('saves outlet successfully when authorized', function () {
|
||||
$permission = Permission::create(['name' => 'create outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->operational()->raw();
|
||||
$facilities = Facility::factory()->count(5)->raw();
|
||||
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
$images = [];
|
||||
|
||||
for ($i = 1; $i <= 5; $i++) {
|
||||
$images[] = makeLivewireFile("image{$i}.jpg");
|
||||
}
|
||||
|
||||
$openingHours = collect(Day::cases())
|
||||
->mapWithKeys(fn ($day) => [
|
||||
$day->value => [
|
||||
'day' => $day->value,
|
||||
'open_time' => fake()->time('H:i'),
|
||||
'close_time' => fake()->time('H:i'),
|
||||
],
|
||||
])
|
||||
->toArray();
|
||||
|
||||
$data = array_merge($outlet, [
|
||||
'opening_hours' => $openingHours,
|
||||
'facilities' => collect($facilities)->pluck('name')->toArray(),
|
||||
'featured_image' => [$featuredImage],
|
||||
'images' => $images,
|
||||
]);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', $data['name'])
|
||||
->set('form.phone_number', $data['phone_number'])
|
||||
->set('form.address', $data['address'])
|
||||
->set('form.landmark', $data['landmark'])
|
||||
->set('form.maps_url', $data['maps_url'])
|
||||
->set('form.opened_date', $data['opened_date'])
|
||||
->set('form.status', $data['status']->value)
|
||||
->set('form.opening_hours', $data['opening_hours'])
|
||||
->set('form.facilities', $data['facilities'])
|
||||
->set('form.featured_image', $data['featured_image'])
|
||||
->set('form.images', $data['images'])
|
||||
->call('save')
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect(Index::class);
|
||||
|
||||
$outlet = Outlet::first();
|
||||
|
||||
$this->assertDatabaseHas('outlets', [
|
||||
'id' => $outlet->id,
|
||||
'name' => $data['name'],
|
||||
'phone_number' => $data['phone_number'],
|
||||
]);
|
||||
|
||||
$this->assertCount(1, $outlet->getMedia('featured_image'));
|
||||
$this->assertCount(5, $outlet->getMedia('images'));
|
||||
|
||||
expect($outlet->getFirstMedia('featured_image')->getPath())->toBeFile();
|
||||
foreach ($outlet->getMedia('images') as $media) {
|
||||
expect($media->getPath())->toBeFile();
|
||||
}
|
||||
});
|
||||
|
||||
it('validates required fields', function () {
|
||||
$permission = Permission::create(['name' => 'create outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->call('save')
|
||||
->assertHasErrors([
|
||||
'form.name',
|
||||
'form.phone_number',
|
||||
'form.address',
|
||||
'form.landmark',
|
||||
'form.opened_date',
|
||||
'form.featured_image',
|
||||
'form.images',
|
||||
]);
|
||||
});
|
||||
|
||||
it('validates name field max length', function () {
|
||||
$permission = Permission::create(['name' => 'create outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', str_repeat('a', 101))
|
||||
->call('save')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('validates facilities minimum count', function () {
|
||||
$permission = Permission::create(['name' => 'create outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.facilities', [])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.facilities']);
|
||||
});
|
||||
|
||||
it('validates featured image max count', function () {
|
||||
$permission = Permission::create(['name' => 'create outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$featuredImages = [
|
||||
makeLivewireFile('featured1.jpg'),
|
||||
makeLivewireFile('featured2.jpg'),
|
||||
];
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.featured_image', $featuredImages)
|
||||
->call('save')
|
||||
->assertHasErrors(['form.featured_image']);
|
||||
});
|
||||
|
||||
it('validates images max count', function () {
|
||||
$permission = Permission::create(['name' => 'create outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$images = [];
|
||||
for ($i = 1; $i <= 6; $i++) {
|
||||
$images[] = makeLivewireFile("image{$i}.jpg");
|
||||
}
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasErrors(['form.images']);
|
||||
});
|
||||
|
||||
it('has default facilities value', function () {
|
||||
$component = mountCreateComponent($this->user);
|
||||
|
||||
expect($component->form->facilities)->toBe(['Parkir']);
|
||||
});
|
||||
|
||||
it('initializes opening hours for all days of week', function () {
|
||||
$component = mountCreateComponent($this->user);
|
||||
|
||||
foreach (Day::cases() as $day) {
|
||||
expect(array_key_exists($day->value, $component->form->opening_hours))->toBeTrue();
|
||||
expect($component->form->opening_hours[$day->value])->toHaveKeys(['open_time', 'close_time']);
|
||||
expect($component->form->opening_hours[$day->value]['open_time'])->toBeNull();
|
||||
expect($component->form->opening_hours[$day->value]['close_time'])->toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it('creates outlet with opening hours for all days', function () {
|
||||
$permission = Permission::create(['name' => 'create outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->operational()->raw();
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
$images = [makeLivewireFile('image1.jpg')];
|
||||
|
||||
$openingHours = collect(Day::cases())
|
||||
->mapWithKeys(fn ($day) => [
|
||||
$day->value => [
|
||||
'open_time' => '09:00',
|
||||
'close_time' => '21:00',
|
||||
],
|
||||
])
|
||||
->toArray();
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', $outlet['name'])
|
||||
->set('form.phone_number', $outlet['phone_number'])
|
||||
->set('form.address', $outlet['address'])
|
||||
->set('form.landmark', $outlet['landmark'])
|
||||
->set('form.opened_date', $outlet['opened_date'])
|
||||
->set('form.status', $outlet['status']->value)
|
||||
->set('form.opening_hours', $openingHours)
|
||||
->set('form.facilities', ['Parkir'])
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$createdOutlet = Outlet::first();
|
||||
expect($createdOutlet->openingHours)->toHaveCount(count(Day::cases()));
|
||||
|
||||
// Verify opening hours data
|
||||
foreach ($createdOutlet->openingHours as $openingHour) {
|
||||
expect($openingHour->open_time)->toBe('09:00');
|
||||
expect($openingHour->close_time)->toBe('21:00');
|
||||
}
|
||||
});
|
||||
|
||||
it('creates outlet with facilities', function () {
|
||||
$permission = Permission::create(['name' => 'create outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->operational()->raw();
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
$images = [makeLivewireFile('image1.jpg')];
|
||||
$facilities = ['Parkir', 'WiFi', 'AC'];
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', $outlet['name'])
|
||||
->set('form.phone_number', $outlet['phone_number'])
|
||||
->set('form.address', $outlet['address'])
|
||||
->set('form.landmark', $outlet['landmark'])
|
||||
->set('form.opened_date', $outlet['opened_date'])
|
||||
->set('form.status', $outlet['status']->value)
|
||||
->set('form.facilities', $facilities)
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$createdOutlet = Outlet::first();
|
||||
expect($createdOutlet->facilities->pluck('name')->toArray())->toBe($facilities);
|
||||
});
|
||||
|
||||
it('sets closed_date when status is TERMINATED', function () {
|
||||
$permission = Permission::create(['name' => 'create outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->operational()->raw();
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
$images = [makeLivewireFile('image1.jpg')];
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', $outlet['name'])
|
||||
->set('form.phone_number', $outlet['phone_number'])
|
||||
->set('form.address', $outlet['address'])
|
||||
->set('form.landmark', $outlet['landmark'])
|
||||
->set('form.opened_date', $outlet['opened_date'])
|
||||
->set('form.status', OutletStatus::TERMINATED->value)
|
||||
->set('form.facilities', ['Parkir'])
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$createdOutlet = Outlet::first();
|
||||
expect($createdOutlet->status->value)->toBe(OutletStatus::TERMINATED->value);
|
||||
expect($createdOutlet->closed_date)->not->toBeNull();
|
||||
});
|
||||
|
||||
it('does not set closed_date for non-terminated status', function () {
|
||||
$permission = Permission::create(['name' => 'create outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->operational()->raw();
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
$images = [makeLivewireFile('image1.jpg')];
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', $outlet['name'])
|
||||
->set('form.phone_number', $outlet['phone_number'])
|
||||
->set('form.address', $outlet['address'])
|
||||
->set('form.landmark', $outlet['landmark'])
|
||||
->set('form.opened_date', $outlet['opened_date'])
|
||||
->set('form.status', OutletStatus::OPERATIONAL->value)
|
||||
->set('form.facilities', ['Parkir'])
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$createdOutlet = Outlet::first();
|
||||
expect($createdOutlet->closed_date)->toBeNull();
|
||||
});
|
||||
|
||||
it('can add and remove facilities dynamically', function () {
|
||||
$component = mountCreateComponent($this->user);
|
||||
|
||||
// Initial state
|
||||
expect($component->form->facilities)->toBe(['Parkir']);
|
||||
|
||||
// Add facility
|
||||
$component->call('addFacility');
|
||||
expect($component->form->facilities)->toBe(['Parkir', '']);
|
||||
|
||||
// Add another facility
|
||||
$component->call('addFacility');
|
||||
expect($component->form->facilities)->toBe(['Parkir', '', '']);
|
||||
|
||||
// Remove middle facility
|
||||
$component->call('removeFacility', 1);
|
||||
expect($component->form->facilities)->toBe(['Parkir', '']);
|
||||
|
||||
// Remove first facility (should keep at least one empty)
|
||||
$component->call('removeFacility', 0);
|
||||
expect($component->form->facilities)->toBe(['']);
|
||||
});
|
||||
|
||||
it('validates maps_url regex pattern', function () {
|
||||
$permission = Permission::create(['name' => 'create outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->operational()->raw();
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
$images = [makeLivewireFile('image1.jpg')];
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', $outlet['name'])
|
||||
->set('form.phone_number', $outlet['phone_number'])
|
||||
->set('form.address', $outlet['address'])
|
||||
->set('form.landmark', $outlet['landmark'])
|
||||
->set('form.maps_url', 'invalid-url')
|
||||
->set('form.opened_date', $outlet['opened_date'])
|
||||
->set('form.facilities', ['Parkir'])
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasErrors(['form.maps_url']);
|
||||
|
||||
// Valid Google Maps URL should pass
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', $outlet['name'])
|
||||
->set('form.phone_number', $outlet['phone_number'])
|
||||
->set('form.address', $outlet['address'])
|
||||
->set('form.landmark', $outlet['landmark'])
|
||||
->set('form.maps_url', 'https://maps.app.goo.gl/example')
|
||||
->set('form.opened_date', $outlet['opened_date'])
|
||||
->set('form.facilities', ['Parkir'])
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
});
|
||||
|
||||
it('handles facility removal edge cases', function () {
|
||||
$component = mountCreateComponent($this->user);
|
||||
|
||||
// Set custom facilities
|
||||
$component->set('form.facilities', ['Parkir', 'WiFi', 'AC']);
|
||||
|
||||
// Remove middle facility
|
||||
$component->call('removeFacility', 1);
|
||||
expect($component->form->facilities)->toBe(['Parkir', 'AC']);
|
||||
|
||||
// Try to remove non-existent index (should not crash)
|
||||
$component->call('removeFacility', 99);
|
||||
expect($component->form->facilities)->toBe(['Parkir', 'AC']);
|
||||
});
|
||||
|
||||
it('creates outlet with soft delete capability', function () {
|
||||
$permission = Permission::create(['name' => 'create outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->operational()->raw();
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
$images = [makeLivewireFile('image1.jpg')];
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.name', $outlet['name'])
|
||||
->set('form.phone_number', $outlet['phone_number'])
|
||||
->set('form.address', $outlet['address'])
|
||||
->set('form.landmark', $outlet['landmark'])
|
||||
->set('form.opened_date', $outlet['opened_date'])
|
||||
->set('form.facilities', ['Parkir'])
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$createdOutlet = Outlet::first();
|
||||
expect($createdOutlet)->toBeInstanceOf(Outlet::class);
|
||||
|
||||
// Verify soft delete trait
|
||||
$createdOutlet->delete();
|
||||
expect(Outlet::withTrashed()->find($createdOutlet->id))->not->toBeNull();
|
||||
expect(Outlet::find($createdOutlet->id))->toBeNull();
|
||||
});
|
||||
@ -1,346 +0,0 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__.'/TestHelpers.php';
|
||||
|
||||
use App\Enums\Day;
|
||||
use App\Enums\OutletStatus;
|
||||
use App\Livewire\Studio\Master\Outlet\Edit;
|
||||
use App\Livewire\Studio\Master\Outlet\Index;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Facility;
|
||||
use App\Models\OpeningHour;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
});
|
||||
|
||||
function mountEditComponent(User $user, Outlet $outlet)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(Edit::class, ['outlet' => $outlet]);
|
||||
}
|
||||
|
||||
it('renders page successfully', function () {
|
||||
$outlet = Outlet::factory()->operational()->create();
|
||||
|
||||
mountEditComponent($this->user, $outlet)
|
||||
->assertViewIs('livewire.studio.master.outlet.form')
|
||||
->assertViewHas('pageTitle', 'Ubah Outlet');
|
||||
});
|
||||
|
||||
it('mounts days and sets form with outlet', function () {
|
||||
$outlet = Outlet::factory()->operational()->create();
|
||||
$component = mountEditComponent($this->user, $outlet);
|
||||
|
||||
$days = Day::cases();
|
||||
expect($component->days)->toHaveCount(count($days));
|
||||
expect($component->form->outlet->id)->toBe($outlet->id);
|
||||
});
|
||||
|
||||
it('prevents update when user is unauthorized', function () {
|
||||
$outlet = Outlet::factory()->operational()->create();
|
||||
Gate::define('update outlet', fn () => false);
|
||||
|
||||
mountEditComponent($this->user, $outlet)
|
||||
->call('save')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('updates outlet successfully when authorized', function () {
|
||||
$permission = Permission::create(['name' => 'update outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outletModel = Outlet::factory()->operational()->create(); // model instance
|
||||
$outletData = Outlet::factory()->operational()->raw(); // data for update
|
||||
$facilities = Facility::factory()->count(5)->raw();
|
||||
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
for ($i = 1; $i <= 5; $i++) {
|
||||
$images[] = makeLivewireFile("image{$i}.jpg");
|
||||
}
|
||||
|
||||
$openingHours = collect(Day::cases())
|
||||
->mapWithKeys(fn ($day) => [
|
||||
$day->value => [
|
||||
'day' => $day->value,
|
||||
'open_time' => fake()->time('H:i'),
|
||||
'close_time' => fake()->time('H:i'),
|
||||
],
|
||||
])
|
||||
->toArray();
|
||||
|
||||
$data = array_merge($outletData, [
|
||||
'opening_hours' => $openingHours,
|
||||
'facilities' => collect($facilities)->pluck('name')->toArray(),
|
||||
'featured_image' => [$featuredImage],
|
||||
'images' => $images,
|
||||
]);
|
||||
|
||||
mountEditComponent($this->user, $outletModel)
|
||||
->set('form.name', $data['name'])
|
||||
->set('form.phone_number', $data['phone_number'])
|
||||
->set('form.address', $data['address'])
|
||||
->set('form.landmark', $data['landmark'])
|
||||
->set('form.maps_url', $data['maps_url'])
|
||||
->set('form.opened_date', $data['opened_date'])
|
||||
->set('form.status', $data['status']->value)
|
||||
->set('form.opening_hours', $data['opening_hours'])
|
||||
->set('form.facilities', $data['facilities'])
|
||||
->set('form.featured_image', $data['featured_image'])
|
||||
->set('form.images', $data['images'])
|
||||
->call('save')
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect(Index::class);
|
||||
|
||||
$outletModel->refresh();
|
||||
|
||||
$this->assertDatabaseHas('outlets', [
|
||||
'id' => $outletModel->id,
|
||||
'name' => $data['name'],
|
||||
'phone_number' => $data['phone_number'],
|
||||
]);
|
||||
|
||||
$this->assertCount(1, $outletModel->getMedia('featured_image'));
|
||||
$this->assertCount(5, $outletModel->getMedia('images'));
|
||||
|
||||
expect($outletModel->getFirstMedia('featured_image')->getPath())->toBeFile();
|
||||
foreach ($outletModel->getMedia('images') as $media) {
|
||||
expect($media->getPath())->toBeFile();
|
||||
}
|
||||
});
|
||||
|
||||
it('loads outlet data into form correctly', function () {
|
||||
$outlet = Outlet::factory()
|
||||
->operational()
|
||||
->has(OpeningHour::factory()->count(7))
|
||||
->has(Facility::factory()->count(3))
|
||||
->create();
|
||||
|
||||
$component = mountEditComponent($this->user, $outlet);
|
||||
|
||||
expect($component->form->outlet->id)->toBe($outlet->id);
|
||||
expect($component->form->name)->toBe($outlet->name);
|
||||
expect($component->form->phone_number)->toBe($outlet->phone_number);
|
||||
expect($component->form->address)->toBe($outlet->address);
|
||||
expect($component->form->landmark)->toBe($outlet->landmark);
|
||||
expect($component->form->status)->toBe($outlet->status->value);
|
||||
expect($component->form->facilities)->toBe($outlet->facilities->pluck('name')->toArray());
|
||||
});
|
||||
|
||||
it('loads opening hours into form correctly', function () {
|
||||
$outlet = Outlet::factory()->operational()->create();
|
||||
|
||||
// Create specific opening hours
|
||||
$openingHours = [];
|
||||
foreach (Day::cases() as $day) {
|
||||
$openingHours[] = OpeningHour::factory()->create([
|
||||
'outlet_id' => $outlet->id,
|
||||
'day' => $day,
|
||||
'open_time' => '09:00',
|
||||
'close_time' => '21:00',
|
||||
]);
|
||||
}
|
||||
|
||||
$component = mountEditComponent($this->user, $outlet);
|
||||
|
||||
foreach (Day::cases() as $day) {
|
||||
expect($component->form->opening_hours[$day->value]['open_time'])->toBe('09:00');
|
||||
expect($component->form->opening_hours[$day->value]['close_time'])->toBe('21:00');
|
||||
}
|
||||
});
|
||||
|
||||
it('updates outlet with partial data changes', function () {
|
||||
$permission = Permission::create(['name' => 'update outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()
|
||||
->operational()
|
||||
->has(Facility::factory()->count(2))
|
||||
->create();
|
||||
|
||||
$newName = 'Updated Outlet Name';
|
||||
$images = [makeLivewireFile('image1.jpg')];
|
||||
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
|
||||
mountEditComponent($this->user, $outlet)
|
||||
->set('form.name', $newName)
|
||||
->set('form.opening_hours', makeValidOpeningHours())
|
||||
->set('form.facilities', ['Parkir'])
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect(Index::class);
|
||||
|
||||
$outlet->refresh();
|
||||
expect($outlet->name)->toBe($newName);
|
||||
expect($outlet->phone_number)->toBe($outlet->phone_number); // Unchanged
|
||||
});
|
||||
|
||||
it('updates outlet status to TERMINATED and sets closed_date', function () {
|
||||
$permission = Permission::create(['name' => 'update outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->operational()->create();
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
$images = [makeLivewireFile('image1.jpg')];
|
||||
|
||||
mountEditComponent($this->user, $outlet)
|
||||
->set('form.status', OutletStatus::TERMINATED->value)
|
||||
->set('form.opening_hours', makeValidOpeningHours())
|
||||
->set('form.facilities', ['Parkir'])
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$outlet->refresh();
|
||||
expect($outlet->status->value)->toBe(OutletStatus::TERMINATED->value);
|
||||
expect($outlet->closed_date)->not->toBeNull();
|
||||
});
|
||||
|
||||
it('updates facilities correctly', function () {
|
||||
$permission = Permission::create(['name' => 'update outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()
|
||||
->operational()
|
||||
->has(Facility::factory()->count(2))
|
||||
->create();
|
||||
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
$images = [makeLivewireFile('image1.jpg')];
|
||||
$newFacilities = ['WiFi', 'AC', 'Parking'];
|
||||
|
||||
mountEditComponent($this->user, $outlet)
|
||||
->set('form.facilities', $newFacilities)
|
||||
->set('form.opening_hours', makeValidOpeningHours())
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$outlet->refresh();
|
||||
expect($outlet->facilities->pluck('name')->toArray())->toBe($newFacilities);
|
||||
});
|
||||
|
||||
it('updates opening hours correctly', function () {
|
||||
$permission = Permission::create(['name' => 'update outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->operational()->create();
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
$images = [makeLivewireFile('image1.jpg')];
|
||||
|
||||
$newOpeningHours = makeValidOpeningHours();
|
||||
|
||||
mountEditComponent($this->user, $outlet)
|
||||
->set('form.opening_hours', $newOpeningHours)
|
||||
->set('form.facilities', ['Parkir'])
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$outlet->refresh();
|
||||
expect($outlet->openingHours)->toHaveCount(count(Day::cases()));
|
||||
|
||||
foreach ($outlet->openingHours as $openingHour) {
|
||||
expect($openingHour->open_time)->toBe('09:00');
|
||||
expect($openingHour->close_time)->toBe('21:00');
|
||||
}
|
||||
});
|
||||
|
||||
it('validates required fields on update', function () {
|
||||
$permission = Permission::create(['name' => 'update outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->operational()->create();
|
||||
|
||||
mountEditComponent($this->user, $outlet)
|
||||
->set('form.name', '') // Empty required field
|
||||
->call('save')
|
||||
->assertHasErrors(['form.name']);
|
||||
});
|
||||
|
||||
it('maintains outlet relationships integrity', function () {
|
||||
$permission = Permission::create(['name' => 'update outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()
|
||||
->operational()
|
||||
->has(Facility::factory()->count(3))
|
||||
->create();
|
||||
|
||||
$originalOpeningHoursCount = count(Day::cases()); // Should be 7 after setOutlet
|
||||
$originalFacilitiesCount = 1; // We set facilities to ['Parkir'] in the test
|
||||
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
$images = [makeLivewireFile('image1.jpg')];
|
||||
|
||||
mountEditComponent($this->user, $outlet)
|
||||
->set('form.name', 'Updated Name')
|
||||
->set('form.opening_hours', makeValidOpeningHours())
|
||||
->set('form.facilities', ['Parkir'])
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$outlet->refresh();
|
||||
expect($outlet->openingHours)->toHaveCount($originalOpeningHoursCount);
|
||||
expect($outlet->facilities)->toHaveCount($originalFacilitiesCount);
|
||||
});
|
||||
|
||||
it('handles outlet with no existing media', function () {
|
||||
$permission = Permission::create(['name' => 'update outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->operational()->create();
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
$images = [makeLivewireFile('image1.jpg')];
|
||||
|
||||
mountEditComponent($this->user, $outlet)
|
||||
->set('form.opening_hours', makeValidOpeningHours())
|
||||
->set('form.facilities', ['Parkir'])
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$outlet->refresh();
|
||||
expect($outlet->getMedia('featured_image'))->toHaveCount(1);
|
||||
});
|
||||
|
||||
it('preserves outlet slug when updating', function () {
|
||||
$permission = Permission::create(['name' => 'update outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->operational()->create();
|
||||
$originalSlug = $outlet->slug;
|
||||
$featuredImage = makeLivewireFile('featured.jpg');
|
||||
$images = [makeLivewireFile('image1.jpg')];
|
||||
|
||||
mountEditComponent($this->user, $outlet)
|
||||
->set('form.phone_number', '0812 3456 7890') // Change non-slug field
|
||||
->set('form.opening_hours', makeValidOpeningHours())
|
||||
->set('form.facilities', ['Parkir'])
|
||||
->set('form.featured_image', [$featuredImage])
|
||||
->set('form.images', $images)
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$outlet->refresh();
|
||||
expect($outlet->slug)->toBe($originalSlug);
|
||||
});
|
||||
@ -1,239 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\OutletStatus;
|
||||
use App\Livewire\Studio\Master\Outlet\Index;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Facility;
|
||||
use App\Models\OpeningHour;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Livewire\Features\SupportTesting\Testable;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
});
|
||||
|
||||
function createOutlet(array $attributes = []): Outlet
|
||||
{
|
||||
return Outlet::factory()
|
||||
->has(OpeningHour::factory()->count(7))
|
||||
->has(Facility::factory()->count(5))
|
||||
->state(fn () => array_merge([
|
||||
'status' => fake()->randomElement(OutletStatus::cases()),
|
||||
], $attributes))
|
||||
->create();
|
||||
}
|
||||
|
||||
function mountIndexComponent(User $user): Testable
|
||||
{
|
||||
return Livewire::actingAs($user)->test(Index::class);
|
||||
}
|
||||
|
||||
it('renders page successfully', function () {
|
||||
mountIndexComponent($this->user)
|
||||
->assertViewIs('livewire.studio.master.outlet.index')
|
||||
->assertViewHas('pageTitle', 'Outlet');
|
||||
});
|
||||
|
||||
it('loads outlets as Eloquent collection', function () {
|
||||
$outlet = createOutlet();
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect($component->get('outlets'))
|
||||
->toBeInstanceOf(Collection::class)
|
||||
->and($component->get('outlets')->first()->id)->toBe($outlet->id);
|
||||
});
|
||||
|
||||
it('loads outlets with relationships', function () {
|
||||
$outlet = createOutlet();
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
$loadedOutlet = $component->get('outlets')->first();
|
||||
|
||||
expect($loadedOutlet->openingHours)->toHaveCount(7)
|
||||
->and($loadedOutlet->facilities)->toHaveCount(5);
|
||||
});
|
||||
|
||||
it('displays all outlets', function () {
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
createOutlet();
|
||||
}
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect($component->get('outlets'))->toHaveCount(5);
|
||||
expect($component->get('outlets')->first())->toBeInstanceOf(Outlet::class);
|
||||
});
|
||||
|
||||
it('prevents delete when user is unauthorized', function () {
|
||||
$outlet = createOutlet();
|
||||
Gate::define('delete outlet', fn () => false);
|
||||
|
||||
mountIndexComponent($this->user)
|
||||
->call('delete', $outlet)
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('deletes an outlet successfully when authorized', function () {
|
||||
$permission = Permission::create(['name' => 'delete outlet']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = createOutlet();
|
||||
|
||||
mountIndexComponent($this->user)
|
||||
->call('delete', $outlet)
|
||||
->assertHasNoErrors();
|
||||
|
||||
$this->assertSoftDeleted('outlets', ['id' => $outlet->id]);
|
||||
});
|
||||
|
||||
it('filters outlets by search query', function () {
|
||||
$outlet1 = createOutlet(['name' => 'Outlet Jakarta']);
|
||||
$outlet2 = createOutlet(['name' => 'Outlet Bandung']);
|
||||
$outlet3 = createOutlet(['name' => 'Toko Surabaya']);
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect($component->get('outlets'))->toHaveCount(3);
|
||||
|
||||
// Test filtering by Jakarta
|
||||
$component->set('search', 'Jakarta');
|
||||
$filteredComponent = mountIndexComponent($this->user)->set('search', 'Jakarta');
|
||||
|
||||
expect($filteredComponent->get('outlets'))->toHaveCount(1);
|
||||
expect($filteredComponent->get('outlets')->first()->name)->toBe('Outlet Jakarta');
|
||||
|
||||
// Test filtering by Outlet
|
||||
$filteredComponent2 = mountIndexComponent($this->user)->set('search', 'Outlet');
|
||||
|
||||
expect($filteredComponent2->get('outlets'))->toHaveCount(2);
|
||||
});
|
||||
|
||||
it('filters outlets by status', function () {
|
||||
$outlet1 = createOutlet(['status' => OutletStatus::OPERATIONAL]);
|
||||
$outlet2 = createOutlet(['status' => OutletStatus::UNDER_CONSTRUCTION]);
|
||||
$outlet3 = createOutlet(['status' => OutletStatus::TERMINATED]);
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect($component->get('outlets'))->toHaveCount(3);
|
||||
|
||||
$filteredComponent = mountIndexComponent($this->user)->set('status', [OutletStatus::OPERATIONAL->value]);
|
||||
|
||||
expect($filteredComponent->get('outlets'))->toHaveCount(1);
|
||||
expect($filteredComponent->get('outlets')->first()->name)->toBe($outlet1->name);
|
||||
|
||||
// Test filtering by multiple statuses
|
||||
$filteredComponent2 = mountIndexComponent($this->user)->set('status', [
|
||||
OutletStatus::OPERATIONAL->value,
|
||||
OutletStatus::UNDER_CONSTRUCTION->value,
|
||||
]);
|
||||
|
||||
expect($filteredComponent2->get('outlets'))->toHaveCount(2);
|
||||
});
|
||||
|
||||
it('filters outlets by both search and status', function () {
|
||||
$outlet1 = createOutlet(['name' => 'Outlet A', 'status' => OutletStatus::OPERATIONAL]);
|
||||
$outlet2 = createOutlet(['name' => 'Outlet B', 'status' => OutletStatus::OPERATIONAL]);
|
||||
$outlet3 = createOutlet(['name' => 'Outlet C', 'status' => OutletStatus::UNDER_CONSTRUCTION]);
|
||||
|
||||
$component = mountIndexComponent($this->user)
|
||||
->set('search', 'Outlet')
|
||||
->set('status', [OutletStatus::OPERATIONAL->value]);
|
||||
|
||||
expect($component->get('outlets'))->toHaveCount(2);
|
||||
});
|
||||
|
||||
it('loads outlets with all required relationships', function () {
|
||||
$outlet = createOutlet();
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
$loadedOutlet = $component->get('outlets')->first();
|
||||
|
||||
expect($loadedOutlet->openingHours)->toHaveCount(7)
|
||||
->and($loadedOutlet->facilities)->toHaveCount(5)
|
||||
->and($loadedOutlet->name)->toBe($outlet->name)
|
||||
->and($loadedOutlet->phone_number)->toBe($outlet->phone_number);
|
||||
});
|
||||
|
||||
it('loads outlets ordered by latest', function () {
|
||||
$outlet1 = createOutlet(['name' => 'First']);
|
||||
sleep(1);
|
||||
$outlet2 = createOutlet(['name' => 'Second']);
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect($component->get('outlets')->first()->name)->toBe('Second');
|
||||
expect($component->get('outlets')->last()->name)->toBe('First');
|
||||
});
|
||||
|
||||
it('provides helper methods for view formatting', function () {
|
||||
$outlet = createOutlet();
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
// Test that outlets are loaded with proper data structure
|
||||
$loadedOutlet = $component->get('outlets')->first();
|
||||
expect($loadedOutlet)->toBeInstanceOf(Outlet::class);
|
||||
expect($loadedOutlet->openingHours)->toBeInstanceOf(Collection::class);
|
||||
expect($loadedOutlet->facilities)->toBeInstanceOf(Collection::class);
|
||||
});
|
||||
|
||||
it('handles empty search results gracefully', function () {
|
||||
createOutlet(['name' => 'Existing Outlet']);
|
||||
|
||||
$component = mountIndexComponent($this->user)->set('search', 'NonExistentOutlet');
|
||||
|
||||
expect($component->get('outlets'))->toHaveCount(0);
|
||||
});
|
||||
|
||||
it('maintains search state across component lifecycle', function () {
|
||||
$outlet1 = createOutlet(['name' => 'Test Outlet']);
|
||||
$outlet2 = createOutlet(['name' => 'Another Outlet']);
|
||||
|
||||
$component = mountIndexComponent($this->user)->set('search', 'Test');
|
||||
|
||||
expect($component->get('outlets'))->toHaveCount(1);
|
||||
expect($component->get('search'))->toBe('Test');
|
||||
|
||||
// Re-mount should maintain state
|
||||
$newComponent = mountIndexComponent($this->user)->set('search', 'Test');
|
||||
|
||||
expect($newComponent->get('outlets'))->toHaveCount(1);
|
||||
});
|
||||
|
||||
it('handles status filter with empty array', function () {
|
||||
createOutlet(['status' => OutletStatus::OPERATIONAL]);
|
||||
createOutlet(['status' => OutletStatus::TERMINATED]);
|
||||
|
||||
$component = mountIndexComponent($this->user)->set('status', []);
|
||||
|
||||
expect($component->get('outlets'))->toHaveCount(2);
|
||||
});
|
||||
|
||||
it('loads outlets efficiently with eager loading', function () {
|
||||
$outlets = collect();
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$outlets->push(createOutlet());
|
||||
}
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
// Verify relationships are loaded
|
||||
$loadedOutlets = $component->get('outlets');
|
||||
foreach ($loadedOutlets as $loadedOutlet) {
|
||||
expect($loadedOutlet->relationLoaded('openingHours'))->toBeTrue();
|
||||
expect($loadedOutlet->relationLoaded('facilities'))->toBeTrue();
|
||||
}
|
||||
});
|
||||
@ -1,37 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\Day;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
function makeLivewireFile(string $name)
|
||||
{
|
||||
$file = UploadedFile::fake()->image($name, 100, 100);
|
||||
$tmpId = Str::random(32);
|
||||
$filename = $tmpId.'-'.base64_encode($file->getClientOriginalName()).'.'.$file->getClientOriginalExtension();
|
||||
$path = "livewire-tmp/{$filename}";
|
||||
|
||||
Storage::disk('local')->putFileAs('livewire-tmp', $file, $filename);
|
||||
|
||||
return [
|
||||
'tmpFilename' => $filename,
|
||||
'name' => $file->getClientOriginalName(),
|
||||
'extension' => $file->getClientOriginalExtension(),
|
||||
'path' => storage_path("app/private/{$path}"),
|
||||
'temporaryUrl' => url("livewire/preview-file/{$filename}"),
|
||||
'size' => $file->getSize(),
|
||||
];
|
||||
}
|
||||
|
||||
function makeValidOpeningHours()
|
||||
{
|
||||
return collect(Day::cases())
|
||||
->mapWithKeys(fn ($day) => [
|
||||
$day->value => [
|
||||
'open_time' => '09:00',
|
||||
'close_time' => '21:00',
|
||||
],
|
||||
])
|
||||
->toArray();
|
||||
}
|
||||
@ -1,651 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\Gender;
|
||||
use App\Enums\UserStatus;
|
||||
use App\Livewire\Studio\Master\User\Create;
|
||||
use App\Livewire\Studio\Master\User\Index;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Features\SupportTesting\Testable;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
});
|
||||
|
||||
function mountCreateComponent(User $user): Testable
|
||||
{
|
||||
return Livewire::actingAs($user)->test(Create::class);
|
||||
}
|
||||
|
||||
it('renders page successfully', function () {
|
||||
mountCreateComponent($this->user)
|
||||
->assertViewIs('livewire.studio.master.user.form')
|
||||
->assertViewHas('pageTitle', 'Tambah Pegawai');
|
||||
});
|
||||
|
||||
it('loads outlets and roles on mount', function () {
|
||||
$outlet = Outlet::factory()->create();
|
||||
$role = Role::create(['name' => 'Admin']);
|
||||
|
||||
$component = mountCreateComponent($this->user);
|
||||
|
||||
expect($component->outlets)->toBeArray();
|
||||
expect($component->outlets)->toHaveKey($outlet->id);
|
||||
expect($component->roles)->toBeArray();
|
||||
expect($component->roles)->toHaveKey($role->id);
|
||||
});
|
||||
|
||||
it('excludes developer role from roles list', function () {
|
||||
Role::create(['name' => 'Developer']);
|
||||
$adminRole = Role::create(['name' => 'Admin']);
|
||||
|
||||
$component = mountCreateComponent($this->user);
|
||||
|
||||
expect($component->roles)->not->toHaveKey(Role::where('name', 'Developer')->first()->id);
|
||||
expect($component->roles)->toHaveKey($adminRole->id);
|
||||
});
|
||||
|
||||
it('prevents create when user is unauthorized', function () {
|
||||
Gate::define('create user', fn () => false);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->call('save')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('creates user successfully when authorized', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->create();
|
||||
$role = Role::create(['name' => 'Admin']);
|
||||
|
||||
$defaultPassword = config('myconfig.password_default');
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.full_name', 'John Doe')
|
||||
->set('form.username', 'johndoe')
|
||||
->set('form.email', 'john@example.com')
|
||||
->set('form.phone_number', '0812 3456 7890')
|
||||
->set('form.base_salary', '5000000')
|
||||
->set('form.birthdate', now()->subYears(25)->format('Y-m-d'))
|
||||
->set('form.hire_date', now()->subMonths(6)->format('Y-m-d'))
|
||||
->set('form.gender', Gender::MALE->value)
|
||||
->set('form.status', UserStatus::ACTIVE->value)
|
||||
->set('form.outlet_ids', [$outlet->id])
|
||||
->set('form.role_ids', [$role->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect(Index::class);
|
||||
|
||||
$createdUser = User::where('email', 'john@example.com')->first();
|
||||
|
||||
expect($createdUser)->not->toBeNull()
|
||||
->and($createdUser->username)->toBe('johndoe')
|
||||
->and($createdUser->status)->toBe(UserStatus::ACTIVE)
|
||||
->and(Hash::check($defaultPassword, $createdUser->password))->toBeTrue();
|
||||
|
||||
$this->assertDatabaseHas('employees', [
|
||||
'user_id' => $createdUser->id,
|
||||
'full_name' => 'John Doe',
|
||||
'gender' => Gender::MALE,
|
||||
]);
|
||||
|
||||
expect($createdUser->outlets)->toHaveCount(1)
|
||||
->and($createdUser->roles)->toHaveCount(1)
|
||||
->and($createdUser->referralCode)->not->toBeNull();
|
||||
});
|
||||
|
||||
it('creates user with inactive status and sets resign date', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->create();
|
||||
$role = Role::create(['name' => 'Admin']);
|
||||
|
||||
$birthdate = now()->subYears(25)->format('Y-m-d');
|
||||
$hireDate = now()->subMonths(6)->format('Y-m-d');
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.full_name', 'Jane Doe')
|
||||
->set('form.username', 'janedoe')
|
||||
->set('form.email', 'jane@example.com')
|
||||
->set('form.phone_number', '0812 3456 7891')
|
||||
->set('form.base_salary', '4.500.000')
|
||||
->set('form.birthdate', $birthdate)
|
||||
->set('form.hire_date', $hireDate)
|
||||
->set('form.gender', Gender::FEMALE->value)
|
||||
->set('form.status', UserStatus::INACTIVE->value)
|
||||
->set('form.outlet_ids', [$outlet->id])
|
||||
->set('form.role_ids', [$role->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$createdUser = User::where('email', 'jane@example.com')->first();
|
||||
$employee = $createdUser->employee;
|
||||
|
||||
expect($createdUser->status)->toBe(UserStatus::INACTIVE);
|
||||
expect($employee->resign_date)->not->toBeNull();
|
||||
});
|
||||
|
||||
it('creates user with multiple outlets', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet1 = Outlet::factory()->create();
|
||||
$outlet2 = Outlet::factory()->create();
|
||||
$outlet3 = Outlet::factory()->create();
|
||||
$role = Role::create(['name' => 'Admin']);
|
||||
|
||||
$birthdate = now()->subYears(25)->format('Y-m-d');
|
||||
$hireDate = now()->subMonths(6)->format('Y-m-d');
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.full_name', 'Multiple Outlet User')
|
||||
->set('form.username', 'multioutlet')
|
||||
->set('form.email', 'multi@example.com')
|
||||
->set('form.phone_number', '0812 3456 7892')
|
||||
->set('form.base_salary', '5.500.000')
|
||||
->set('form.birthdate', $birthdate)
|
||||
->set('form.hire_date', $hireDate)
|
||||
->set('form.gender', Gender::MALE->value)
|
||||
->set('form.status', UserStatus::ACTIVE->value)
|
||||
->set('form.outlet_ids', [$outlet1->id, $outlet2->id, $outlet3->id])
|
||||
->set('form.role_ids', [$role->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$createdUser = User::where('email', 'multi@example.com')->first();
|
||||
|
||||
expect($createdUser->outlets->count())->toBe(3);
|
||||
expect($createdUser->outlets->pluck('id')->toArray())->toContain($outlet1->id, $outlet2->id, $outlet3->id);
|
||||
});
|
||||
|
||||
it('creates user with multiple roles', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->create();
|
||||
$adminRole = Role::create(['name' => 'Admin']);
|
||||
$leaderRole = Role::create(['name' => 'Leader']);
|
||||
|
||||
$birthdate = now()->subYears(25)->format('Y-m-d');
|
||||
$hireDate = now()->subMonths(6)->format('Y-m-d');
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.full_name', 'Multiple Role User')
|
||||
->set('form.username', 'multirole')
|
||||
->set('form.email', 'role@example.com')
|
||||
->set('form.phone_number', '0812 3456 7893')
|
||||
->set('form.base_salary', '6.000.000')
|
||||
->set('form.birthdate', $birthdate)
|
||||
->set('form.hire_date', $hireDate)
|
||||
->set('form.gender', Gender::MALE->value)
|
||||
->set('form.status', UserStatus::ACTIVE->value)
|
||||
->set('form.outlet_ids', [$outlet->id])
|
||||
->set('form.role_ids', [$adminRole->id, $leaderRole->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$createdUser = User::where('email', 'role@example.com')->first();
|
||||
|
||||
expect($createdUser->roles->count())->toBe(2);
|
||||
expect($createdUser->roles->pluck('id')->toArray())->toContain($adminRole->id, $leaderRole->id);
|
||||
});
|
||||
|
||||
it('validates required fields', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$component = mountCreateComponent($this->user);
|
||||
// Don't set any required fields to ensure they're validated
|
||||
$component->call('save')
|
||||
->assertHasErrors([
|
||||
'form.full_name',
|
||||
'form.username',
|
||||
'form.email',
|
||||
'form.phone_number',
|
||||
'form.base_salary',
|
||||
'form.birthdate',
|
||||
'form.hire_date',
|
||||
'form.gender',
|
||||
'form.outlet_ids',
|
||||
'form.role_ids',
|
||||
]);
|
||||
});
|
||||
|
||||
it('validates full_name field max length', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.full_name', str_repeat('a', 101))
|
||||
->call('save')
|
||||
->assertHasErrors(['form.full_name']);
|
||||
});
|
||||
|
||||
it('validates username field required', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.full_name', 'Test User')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.username']);
|
||||
});
|
||||
|
||||
it('validates username field alpha numeric', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.username', 'test-user@123')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.username']);
|
||||
});
|
||||
|
||||
it('validates username field min length', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.username', 'test')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.username']);
|
||||
});
|
||||
|
||||
it('validates username field max length', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.username', str_repeat('a', 21))
|
||||
->call('save')
|
||||
->assertHasErrors(['form.username']);
|
||||
});
|
||||
|
||||
it('validates username field uniqueness', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$existingUser = User::factory()->create(['username' => 'existinguser']);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.username', 'existinguser')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.username']);
|
||||
});
|
||||
|
||||
it('validates email field required', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.full_name', 'Test User')
|
||||
->set('form.username', 'testuser')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.email']);
|
||||
});
|
||||
|
||||
it('validates email field format', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.email', 'invalid-email')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.email']);
|
||||
});
|
||||
|
||||
it('validates email field max length', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.email', str_repeat('a', 90).'@example.com')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.email']);
|
||||
});
|
||||
|
||||
it('validates email field uniqueness', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$existingUser = User::factory()->create(['email' => 'existing@example.com']);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.email', 'existing@example.com')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.email']);
|
||||
});
|
||||
|
||||
it('validates phone_number field required', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.full_name', 'Test User')
|
||||
->set('form.username', 'testuser')
|
||||
->set('form.email', 'test@example.com')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.phone_number']);
|
||||
});
|
||||
|
||||
it('validates base_salary field required', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.full_name', 'Test User')
|
||||
->set('form.username', 'testuser')
|
||||
->set('form.email', 'test@example.com')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.base_salary']);
|
||||
});
|
||||
|
||||
it('validates birthdate field required', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.full_name', 'Test User')
|
||||
->set('form.username', 'testuser')
|
||||
->set('form.email', 'test@example.com')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.birthdate']);
|
||||
});
|
||||
|
||||
it('validates birthdate must be before 18 years ago', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$birthdate = now()->subYears(17)->format('Y-m-d');
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.birthdate', $birthdate)
|
||||
->call('save')
|
||||
->assertHasErrors(['form.birthdate']);
|
||||
});
|
||||
|
||||
it('validates hire_date field required', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.full_name', 'Test User')
|
||||
->set('form.username', 'testuser')
|
||||
->set('form.email', 'test@example.com')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.hire_date']);
|
||||
});
|
||||
|
||||
it('validates gender field required', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.full_name', 'Test User')
|
||||
->set('form.username', 'testuser')
|
||||
->set('form.email', 'test@example.com')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.gender']);
|
||||
});
|
||||
|
||||
it('validates gender field must be valid enum value', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.gender', 999) // Invalid enum value
|
||||
->call('save')
|
||||
->assertHasErrors(['form.gender']);
|
||||
});
|
||||
|
||||
it('validates status field required', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.full_name', 'Test User')
|
||||
->set('form.username', 'testuser')
|
||||
->set('form.email', 'test@example.com')
|
||||
->set('form.status', 99) // Invalid enum value
|
||||
->call('save')
|
||||
->assertHasErrors(['form.status']);
|
||||
});
|
||||
|
||||
it('validates status field must be valid enum value', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.status', '99')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.status']);
|
||||
});
|
||||
|
||||
it('validates outlet_ids field required', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.full_name', 'Test User')
|
||||
->set('form.username', 'testuser')
|
||||
->set('form.email', 'test@example.com')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.outlet_ids']);
|
||||
});
|
||||
|
||||
it('validates outlet_ids must be array', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$this->expectException(TypeError::class);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.outlet_ids', 'not-array')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.outlet_ids']);
|
||||
});
|
||||
|
||||
it('validates outlet_ids minimum count', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.outlet_ids', [])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.outlet_ids']);
|
||||
});
|
||||
|
||||
it('validates outlet_ids must exist', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.outlet_ids', [99999])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.outlet_ids.0']);
|
||||
});
|
||||
|
||||
it('validates role_ids field required', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.full_name', 'Test User')
|
||||
->set('form.username', 'testuser')
|
||||
->set('form.email', 'test@example.com')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.role_ids']);
|
||||
});
|
||||
|
||||
it('validates role_ids must be array', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$this->expectException(TypeError::class);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.role_ids', 'not-array')
|
||||
->call('save')
|
||||
->assertHasErrors(['form.role_ids']);
|
||||
});
|
||||
|
||||
it('validates role_ids minimum count', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.role_ids', [])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.role_ids']);
|
||||
});
|
||||
|
||||
it('validates role_ids must exist', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.role_ids', [99999])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.role_ids.0']);
|
||||
});
|
||||
|
||||
it('creates employee code automatically', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->create();
|
||||
$role = Role::create(['name' => 'Admin']);
|
||||
|
||||
$birthdate = now()->subYears(25)->format('Y-m-d');
|
||||
$hireDate = now()->subMonths(6)->format('Y-m-d');
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.full_name', 'Employee Code Test')
|
||||
->set('form.username', 'empcodetest')
|
||||
->set('form.email', 'empcode@example.com')
|
||||
->set('form.phone_number', '0812 3456 7894')
|
||||
->set('form.base_salary', '4.000.000')
|
||||
->set('form.birthdate', $birthdate)
|
||||
->set('form.hire_date', $hireDate)
|
||||
->set('form.gender', Gender::MALE->value)
|
||||
->set('form.status', UserStatus::ACTIVE->value)
|
||||
->set('form.outlet_ids', [$outlet->id])
|
||||
->set('form.role_ids', [$role->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$createdUser = User::where('email', 'empcode@example.com')->first();
|
||||
$employee = $createdUser->employee;
|
||||
|
||||
expect($employee->code)->toBeString();
|
||||
expect($employee->code)->toStartWith('EMP');
|
||||
});
|
||||
|
||||
it('creates referral code automatically', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->create();
|
||||
$role = Role::create(['name' => 'Admin']);
|
||||
|
||||
$birthdate = now()->subYears(25)->format('Y-m-d');
|
||||
$hireDate = now()->subMonths(6)->format('Y-m-d');
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.full_name', 'Referral Code Test')
|
||||
->set('form.username', 'refcodetest')
|
||||
->set('form.email', 'refcode@example.com')
|
||||
->set('form.phone_number', '0812 3456 7895')
|
||||
->set('form.base_salary', '4.500.000')
|
||||
->set('form.birthdate', $birthdate)
|
||||
->set('form.hire_date', $hireDate)
|
||||
->set('form.gender', Gender::MALE->value)
|
||||
->set('form.status', UserStatus::ACTIVE->value)
|
||||
->set('form.outlet_ids', [$outlet->id])
|
||||
->set('form.role_ids', [$role->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$createdUser = User::where('email', 'refcode@example.com')->first();
|
||||
|
||||
expect($createdUser->referralCode)->not->toBeNull();
|
||||
expect($createdUser->referralCode->code)->toBeString();
|
||||
});
|
||||
|
||||
it('formats base salary correctly when storing', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->create();
|
||||
$role = Role::create(['name' => 'Admin']);
|
||||
|
||||
$birthdate = now()->subYears(25)->format('Y-m-d');
|
||||
$hireDate = now()->subMonths(6)->format('Y-m-d');
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.full_name', 'Salary Format Test')
|
||||
->set('form.username', 'salarytest')
|
||||
->set('form.email', 'salary@example.com')
|
||||
->set('form.phone_number', '0812 3456 7896')
|
||||
->set('form.base_salary', '5.500.000')
|
||||
->set('form.birthdate', $birthdate)
|
||||
->set('form.hire_date', $hireDate)
|
||||
->set('form.gender', Gender::MALE->value)
|
||||
->set('form.status', UserStatus::ACTIVE->value)
|
||||
->set('form.outlet_ids', [$outlet->id])
|
||||
->set('form.role_ids', [$role->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$createdUser = User::where('email', 'salary@example.com')->first();
|
||||
$employee = $createdUser->employee;
|
||||
|
||||
expect($employee->base_salary)->toBe(5500000);
|
||||
});
|
||||
|
||||
it('allows nullable address field', function () {
|
||||
$permission = Permission::create(['name' => 'create user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->create();
|
||||
$role = Role::create(['name' => 'Admin']);
|
||||
|
||||
$birthdate = now()->subYears(25)->format('Y-m-d');
|
||||
$hireDate = now()->subMonths(6)->format('Y-m-d');
|
||||
|
||||
mountCreateComponent($this->user)
|
||||
->set('form.full_name', 'No Address Test')
|
||||
->set('form.username', 'noaddresstest')
|
||||
->set('form.email', 'noaddress@example.com')
|
||||
->set('form.phone_number', '0812 3456 7897')
|
||||
->set('form.base_salary', '4.000.000')
|
||||
->set('form.birthdate', $birthdate)
|
||||
->set('form.hire_date', $hireDate)
|
||||
->set('form.address', null)
|
||||
->set('form.gender', Gender::MALE->value)
|
||||
->set('form.status', UserStatus::ACTIVE->value)
|
||||
->set('form.outlet_ids', [$outlet->id])
|
||||
->set('form.role_ids', [$role->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$createdUser = User::where('email', 'noaddress@example.com')->first();
|
||||
$employee = $createdUser->employee;
|
||||
|
||||
expect($employee->address)->toBeNull();
|
||||
});
|
||||
@ -1,677 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\Gender;
|
||||
use App\Enums\UserStatus;
|
||||
use App\Livewire\Studio\Master\User\Edit;
|
||||
use App\Livewire\Studio\Master\User\Index;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Livewire\Features\SupportTesting\Testable;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
});
|
||||
|
||||
function createEditUser(array $attributes = []): User
|
||||
{
|
||||
return User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->state(fn () => array_merge([
|
||||
'status' => fake()->randomElement(UserStatus::cases()),
|
||||
], $attributes))
|
||||
->create();
|
||||
}
|
||||
|
||||
function mountEditComponent(User $user, User $employeeUser): Testable
|
||||
{
|
||||
return Livewire::actingAs($user)->test(Edit::class, ['user' => $employeeUser]);
|
||||
}
|
||||
|
||||
it('renders page successfully', function () {
|
||||
$employeeUser = createEditUser();
|
||||
\Spatie\Permission\Models\Role::firstOrCreate(['name' => 'Admin']);
|
||||
$employeeUser->assignRole('Admin');
|
||||
|
||||
mountEditComponent($this->user, $employeeUser)
|
||||
->assertViewIs('livewire.studio.master.user.form')
|
||||
->assertViewHas('pageTitle', 'Ubah Pegawai');
|
||||
});
|
||||
|
||||
it('loads outlets and roles on mount', function () {
|
||||
$employeeUser = createEditUser();
|
||||
$outlet = Outlet::factory()->create();
|
||||
$role = Role::create(['name' => 'Admin']);
|
||||
|
||||
$component = mountEditComponent($this->user, $employeeUser);
|
||||
|
||||
expect($component->outlets)->toBeArray();
|
||||
expect($component->outlets)->toHaveKey($outlet->id);
|
||||
expect($component->roles)->toBeArray();
|
||||
expect($component->roles)->toHaveKey($role->id);
|
||||
});
|
||||
|
||||
it('loads existing user data into form', function () {
|
||||
$outlet = Outlet::factory()->create();
|
||||
$role = Role::create(['name' => 'Admin']);
|
||||
|
||||
$employeeUser = createEditUser([
|
||||
'username' => 'existinguser',
|
||||
'email' => 'existing@example.com',
|
||||
'status' => UserStatus::ACTIVE,
|
||||
]);
|
||||
|
||||
$employeeUser->employee->update([
|
||||
'full_name' => 'Existing User',
|
||||
'phone_number' => '081234567890',
|
||||
'base_salary' => 5000000,
|
||||
'birthdate' => '1995-01-15',
|
||||
'hire_date' => '2020-01-01',
|
||||
'address' => 'Existing Address',
|
||||
'gender' => Gender::MALE,
|
||||
]);
|
||||
|
||||
$employeeUser->assignRole($role->id);
|
||||
$employeeUser->outlets()->attach($outlet->id);
|
||||
|
||||
$component = mountEditComponent($this->user, $employeeUser);
|
||||
|
||||
expect($component->form->full_name)->toBe('Existing User')
|
||||
->and($component->form->username)->toBe('existinguser')
|
||||
->and($component->form->email)->toBe('existing@example.com')
|
||||
->and($component->form->phone_number)->toBe('081234567890')
|
||||
->and($component->form->base_salary)->toBe('5000000');
|
||||
expect($component->form->birthdate)->toBe('1995-01-15');
|
||||
expect($component->form->hire_date)->toBe('2020-01-01');
|
||||
expect($component->form->address)->toBe('Existing Address');
|
||||
expect((int) $component->form->gender)->toBe(Gender::MALE->value);
|
||||
expect((int) $component->form->status)->toBe(UserStatus::ACTIVE->value);
|
||||
expect($component->form->outlet_ids)->toContain($outlet->id);
|
||||
expect($component->form->role_ids)->toContain($role->id);
|
||||
});
|
||||
|
||||
it('excludes developer role from roles list', function () {
|
||||
Role::firstOrCreate(['name' => 'Admin']);
|
||||
Role::firstOrCreate(['name' => 'Developer']);
|
||||
|
||||
$role = Role::firstOrCreate(['name' => 'Admin']);
|
||||
$employeeUser = createEditUser();
|
||||
$employeeUser->assignRole($role->id);
|
||||
|
||||
$component = mountEditComponent($this->user, $employeeUser);
|
||||
|
||||
$developerRole = Role::where('name', 'Developer')->first();
|
||||
expect($component->roles)->not->toHaveKey($developerRole->id);
|
||||
});
|
||||
|
||||
it('prevents update when user is unauthorized', function () {
|
||||
Role::firstOrCreate(['name' => 'Admin']);
|
||||
|
||||
$role = Role::firstOrCreate(['name' => 'Admin']);
|
||||
$employeeUser = createEditUser();
|
||||
$employeeUser->assignRole($role->id);
|
||||
|
||||
Gate::define('update user', fn () => false);
|
||||
|
||||
mountEditComponent($this->user, $employeeUser)
|
||||
->call('save')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
it('updates user successfully when authorized', function () {
|
||||
$permission = Permission::create(['name' => 'update user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->create();
|
||||
$role = Role::firstOrCreate(['name' => 'Admin']);
|
||||
|
||||
$employeeUser = createEditUser();
|
||||
$employeeUser->assignRole($role->id);
|
||||
$employeeUser->outlets()->attach($outlet->id);
|
||||
|
||||
$birthdate = now()->subYears(25)->format('Y-m-d');
|
||||
$hireDate = now()->subMonths(6)->format('Y-m-d');
|
||||
|
||||
mountEditComponent($this->user, $employeeUser)
|
||||
->set('form.full_name', 'Updated Name')
|
||||
->set('form.username', 'updateduser')
|
||||
->set('form.email', 'updated@example.com')
|
||||
->set('form.phone_number', '0813 9999 8888')
|
||||
->set('form.base_salary', '6.000.000')
|
||||
->set('form.birthdate', $birthdate)
|
||||
->set('form.hire_date', $hireDate)
|
||||
->set('form.address', 'Updated Address')
|
||||
->set('form.gender', Gender::FEMALE->value)
|
||||
->set('form.status', UserStatus::ACTIVE->value)
|
||||
->set('form.outlet_ids', [$outlet->id])
|
||||
->set('form.role_ids', [$role->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect(Index::class);
|
||||
|
||||
$employeeUser->refresh();
|
||||
$employeeUser->load(['employee', 'outlets', 'roles']);
|
||||
|
||||
expect($employeeUser->username)->toBe('updateduser');
|
||||
expect($employeeUser->email)->toBe('updated@example.com');
|
||||
expect($employeeUser->status)->toBe(UserStatus::ACTIVE);
|
||||
|
||||
expect($employeeUser->employee->full_name)->toBe('Updated Name');
|
||||
expect($employeeUser->employee->phone_number)->toBe('0813 9999 8888');
|
||||
expect($employeeUser->employee->base_salary)->toBe(6000000);
|
||||
expect($employeeUser->employee->address)->toBe('Updated Address');
|
||||
expect($employeeUser->employee->gender)->toBe(Gender::FEMALE);
|
||||
|
||||
expect($employeeUser->outlets->pluck('id')->toArray())->toContain($outlet->id);
|
||||
expect($employeeUser->roles->pluck('id')->toArray())->toContain($role->id);
|
||||
});
|
||||
|
||||
it('updates user with inactive status and sets resign date', function () {
|
||||
$permission = Permission::create(['name' => 'update user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->create();
|
||||
$role = Role::firstOrCreate(['name' => 'Admin']);
|
||||
|
||||
$employeeUser = createEditUser(['status' => UserStatus::ACTIVE]);
|
||||
$employeeUser->assignRole($role->id);
|
||||
$employeeUser->outlets()->attach($outlet->id);
|
||||
|
||||
$birthdate = now()->subYears(25)->format('Y-m-d');
|
||||
$hireDate = now()->subMonths(6)->format('Y-m-d');
|
||||
|
||||
mountEditComponent($this->user, $employeeUser)
|
||||
->set('form.status', UserStatus::INACTIVE->value)
|
||||
->set('form.full_name', $employeeUser->employee->full_name)
|
||||
->set('form.username', $employeeUser->username)
|
||||
->set('form.email', $employeeUser->email)
|
||||
->set('form.phone_number', $employeeUser->employee->phone_number)
|
||||
->set('form.base_salary', '5.000.000')
|
||||
->set('form.birthdate', $birthdate)
|
||||
->set('form.hire_date', $hireDate)
|
||||
->set('form.gender', $employeeUser->employee->gender->value)
|
||||
->set('form.outlet_ids', [$outlet->id])
|
||||
->set('form.role_ids', [$role->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$employeeUser->refresh();
|
||||
$employeeUser->load('employee');
|
||||
|
||||
expect($employeeUser->status)->toBe(UserStatus::INACTIVE);
|
||||
expect($employeeUser->employee->resign_date)->not->toBeNull();
|
||||
});
|
||||
|
||||
it('clears resign date when status changes to active', function () {
|
||||
$permission = Permission::create(['name' => 'update user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->create();
|
||||
$role = Role::firstOrCreate(['name' => 'Admin']);
|
||||
|
||||
$employeeUser = createEditUser([
|
||||
'status' => UserStatus::INACTIVE,
|
||||
]);
|
||||
|
||||
$employeeUser->assignRole($role->id);
|
||||
$employeeUser->outlets()->attach($outlet->id);
|
||||
|
||||
$employee = $employeeUser->employee;
|
||||
$employee->update(['resign_date' => now()->subDays(5)]);
|
||||
|
||||
expect($employee->resign_date)->not->toBeNull();
|
||||
|
||||
$birthdate = $employee->birthdate;
|
||||
$hireDate = $employee->hire_date;
|
||||
|
||||
mountEditComponent($this->user, $employeeUser)
|
||||
->set('form.status', UserStatus::ACTIVE->value)
|
||||
->set('form.full_name', $employee->full_name)
|
||||
->set('form.username', $employeeUser->username)
|
||||
->set('form.email', $employeeUser->email)
|
||||
->set('form.phone_number', $employee->phone_number)
|
||||
->set('form.base_salary', '5.000.000')
|
||||
->set('form.birthdate', $birthdate)
|
||||
->set('form.hire_date', $hireDate)
|
||||
->set('form.gender', $employee->gender->value)
|
||||
->set('form.outlet_ids', [$outlet->id])
|
||||
->set('form.role_ids', [$role->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$employeeUser->refresh();
|
||||
|
||||
expect($employeeUser->status)->toBe(UserStatus::ACTIVE);
|
||||
expect($employee->resign_date)->not->toBeNull();
|
||||
});
|
||||
|
||||
it('updates user with multiple outlets', function () {
|
||||
$permission = Permission::create(['name' => 'update user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet1 = Outlet::factory()->create();
|
||||
$outlet2 = Outlet::factory()->create();
|
||||
$outlet3 = Outlet::factory()->create();
|
||||
$role = Role::firstOrCreate(['name' => 'Admin']);
|
||||
|
||||
$employeeUser = createEditUser();
|
||||
$employeeUser->assignRole($role->id);
|
||||
$employeeUser->outlets()->attach($outlet1->id);
|
||||
|
||||
$birthdate = now()->subYears(25)->format('Y-m-d');
|
||||
$hireDate = now()->subMonths(6)->format('Y-m-d');
|
||||
|
||||
mountEditComponent($this->user, $employeeUser)
|
||||
->set('form.full_name', $employeeUser->employee->full_name)
|
||||
->set('form.username', $employeeUser->username)
|
||||
->set('form.email', $employeeUser->email)
|
||||
->set('form.phone_number', $employeeUser->employee->phone_number)
|
||||
->set('form.base_salary', '5.000.000')
|
||||
->set('form.birthdate', $birthdate)
|
||||
->set('form.hire_date', $hireDate)
|
||||
->set('form.gender', $employeeUser->employee->gender->value)
|
||||
->set('form.status', UserStatus::ACTIVE->value)
|
||||
->set('form.outlet_ids', [$outlet1->id, $outlet2->id, $outlet3->id])
|
||||
->set('form.role_ids', [$role->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$employeeUser->refresh();
|
||||
$employeeUser->load('outlets');
|
||||
|
||||
expect($employeeUser->outlets->count())->toBe(3);
|
||||
expect($employeeUser->outlets->pluck('id')->toArray())->toContain($outlet1->id, $outlet2->id, $outlet3->id);
|
||||
});
|
||||
|
||||
it('updates user with multiple roles', function () {
|
||||
$permission = Permission::create(['name' => 'update user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->create();
|
||||
$adminRole = Role::firstOrCreate(['name' => 'Admin']);
|
||||
$leaderRole = Role::firstOrCreate(['name' => 'Leader']);
|
||||
|
||||
$employeeUser = createEditUser();
|
||||
$employeeUser->assignRole($adminRole->id);
|
||||
$employeeUser->outlets()->attach($outlet->id);
|
||||
|
||||
$birthdate = now()->subYears(25)->format('Y-m-d');
|
||||
$hireDate = now()->subMonths(6)->format('Y-m-d');
|
||||
|
||||
mountEditComponent($this->user, $employeeUser)
|
||||
->set('form.full_name', $employeeUser->employee->full_name)
|
||||
->set('form.username', $employeeUser->username)
|
||||
->set('form.email', $employeeUser->email)
|
||||
->set('form.phone_number', $employeeUser->employee->phone_number)
|
||||
->set('form.base_salary', '5.000.000')
|
||||
->set('form.birthdate', $birthdate)
|
||||
->set('form.hire_date', $hireDate)
|
||||
->set('form.gender', $employeeUser->employee->gender->value)
|
||||
->set('form.status', UserStatus::ACTIVE->value)
|
||||
->set('form.outlet_ids', [$outlet->id])
|
||||
->set('form.role_ids', [$adminRole->id, $leaderRole->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$employeeUser->refresh();
|
||||
$employeeUser->load('roles');
|
||||
|
||||
expect($employeeUser->roles->count())->toBe(2);
|
||||
expect($employeeUser->roles->pluck('id')->toArray())->toContain($adminRole->id, $leaderRole->id);
|
||||
});
|
||||
|
||||
it('syncs outlets correctly when updating', function () {
|
||||
$permission = Permission::create(['name' => 'update user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet1 = Outlet::factory()->create();
|
||||
$outlet2 = Outlet::factory()->create();
|
||||
$outlet3 = Outlet::factory()->create();
|
||||
$role = Role::firstOrCreate(['name' => 'Admin']);
|
||||
|
||||
$employeeUser = createEditUser();
|
||||
$employeeUser->assignRole($role->id);
|
||||
$employeeUser->outlets()->attach([$outlet1->id, $outlet2->id]);
|
||||
|
||||
expect($employeeUser->outlets->count())->toBe(2);
|
||||
|
||||
$birthdate = now()->subYears(25)->format('Y-m-d');
|
||||
$hireDate = now()->subMonths(6)->format('Y-m-d');
|
||||
|
||||
mountEditComponent($this->user, $employeeUser)
|
||||
->set('form.full_name', $employeeUser->employee->full_name)
|
||||
->set('form.username', $employeeUser->username)
|
||||
->set('form.email', $employeeUser->email)
|
||||
->set('form.phone_number', $employeeUser->employee->phone_number)
|
||||
->set('form.base_salary', '5.000.000')
|
||||
->set('form.birthdate', $birthdate)
|
||||
->set('form.hire_date', $hireDate)
|
||||
->set('form.gender', $employeeUser->employee->gender->value)
|
||||
->set('form.status', UserStatus::ACTIVE->value)
|
||||
->set('form.outlet_ids', [$outlet2->id, $outlet3->id])
|
||||
->set('form.role_ids', [$role->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$employeeUser->refresh();
|
||||
$employeeUser->load('outlets');
|
||||
|
||||
expect($employeeUser->outlets->count())->toBe(2);
|
||||
expect($employeeUser->outlets->pluck('id')->toArray())->toContain($outlet2->id, $outlet3->id);
|
||||
expect($employeeUser->outlets->pluck('id')->toArray())->not->toContain($outlet1->id);
|
||||
});
|
||||
|
||||
it('syncs roles correctly when updating', function () {
|
||||
$permission = Permission::create(['name' => 'update user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$outlet = Outlet::factory()->create();
|
||||
$adminRole = Role::firstOrCreate(['name' => 'Admin']);
|
||||
$leaderRole = Role::firstOrCreate(['name' => 'Leader']);
|
||||
$ownerRole = Role::firstOrCreate(['name' => 'Owner']);
|
||||
|
||||
$employeeUser = createEditUser();
|
||||
$employeeUser->assignRole([$adminRole->id, $leaderRole->id]);
|
||||
$employeeUser->outlets()->attach($outlet->id);
|
||||
|
||||
expect($employeeUser->roles->count())->toBe(2);
|
||||
|
||||
$birthdate = now()->subYears(25)->format('Y-m-d');
|
||||
$hireDate = now()->subMonths(6)->format('Y-m-d');
|
||||
|
||||
mountEditComponent($this->user, $employeeUser)
|
||||
->set('form.full_name', $employeeUser->employee->full_name)
|
||||
->set('form.username', $employeeUser->username)
|
||||
->set('form.email', $employeeUser->email)
|
||||
->set('form.phone_number', $employeeUser->employee->phone_number)
|
||||
->set('form.base_salary', '5.000.000')
|
||||
->set('form.birthdate', $birthdate)
|
||||
->set('form.hire_date', $hireDate)
|
||||
->set('form.gender', $employeeUser->employee->gender->value)
|
||||
->set('form.status', UserStatus::ACTIVE->value)
|
||||
->set('form.outlet_ids', [$outlet->id])
|
||||
->set('form.role_ids', [$leaderRole->id, $ownerRole->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$employeeUser->refresh();
|
||||
$employeeUser->load('roles');
|
||||
|
||||
expect($employeeUser->roles->count())->toBe(2);
|
||||
expect($employeeUser->roles->pluck('id')->toArray())->toContain($leaderRole->id, $ownerRole->id);
|
||||
expect($employeeUser->roles->pluck('id')->toArray())->not->toContain($adminRole->id);
|
||||
});
|
||||
|
||||
it('validates required fields', function () {
|
||||
$permission = Permission::create(['name' => 'update user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$role = Role::firstOrCreate(['name' => 'Admin']);
|
||||
$employeeUser = createEditUser();
|
||||
$employeeUser->assignRole($role->id);
|
||||
|
||||
$component = mountEditComponent($this->user, $employeeUser);
|
||||
|
||||
// Clear all fields
|
||||
$component->set('form.full_name', '')
|
||||
->set('form.username', '')
|
||||
->set('form.email', '')
|
||||
->set('form.phone_number', '')
|
||||
->set('form.base_salary', '')
|
||||
->set('form.birthdate', '')
|
||||
->set('form.hire_date', '')
|
||||
->set('form.gender', '')
|
||||
->set('form.outlet_ids', [])
|
||||
->set('form.role_ids', [])
|
||||
->call('save')
|
||||
->assertHasErrors([
|
||||
'form.full_name',
|
||||
'form.username',
|
||||
'form.email',
|
||||
'form.phone_number',
|
||||
'form.base_salary',
|
||||
'form.birthdate',
|
||||
'form.hire_date',
|
||||
'form.gender',
|
||||
'form.outlet_ids',
|
||||
'form.role_ids',
|
||||
]);
|
||||
});
|
||||
|
||||
it('validates username uniqueness ignores current user', function () {
|
||||
$permission = Permission::create(['name' => 'update user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$role = Role::firstOrCreate(['name' => 'Admin']);
|
||||
$employeeUser = createEditUser(['username' => 'existinguser']);
|
||||
$employeeUser->assignRole($role->id);
|
||||
|
||||
$outlet = Outlet::factory()->create();
|
||||
|
||||
$birthdate = now()->subYears(25)->format('Y-m-d');
|
||||
$hireDate = now()->subMonths(6)->format('Y-m-d');
|
||||
|
||||
// Should allow keeping the same username
|
||||
mountEditComponent($this->user, $employeeUser)
|
||||
->set('form.full_name', $employeeUser->employee->full_name)
|
||||
->set('form.username', 'existinguser') // Same username
|
||||
->set('form.email', $employeeUser->email)
|
||||
->set('form.phone_number', $employeeUser->employee->phone_number)
|
||||
->set('form.base_salary', '5.000.000')
|
||||
->set('form.birthdate', $birthdate)
|
||||
->set('form.hire_date', $hireDate)
|
||||
->set('form.gender', $employeeUser->employee->gender->value)
|
||||
->set('form.status', UserStatus::ACTIVE->value)
|
||||
->set('form.outlet_ids', [$outlet->id])
|
||||
->set('form.role_ids', [$role->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors(['form.username']);
|
||||
});
|
||||
|
||||
it('validates username uniqueness prevents duplicate', function () {
|
||||
$permission = Permission::create(['name' => 'update user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$role = Role::firstOrCreate(['name' => 'Admin']);
|
||||
$existingUser = createEditUser(['username' => 'existinguser']);
|
||||
$existingUser->assignRole($role->id);
|
||||
|
||||
$employeeUser = createEditUser(['username' => 'otheruser']);
|
||||
$employeeUser->assignRole($role->id);
|
||||
|
||||
$outlet = Outlet::factory()->create();
|
||||
$role = Role::firstOrCreate(['name' => 'Admin']);
|
||||
|
||||
$birthdate = now()->subYears(25)->format('Y-m-d');
|
||||
$hireDate = now()->subMonths(6)->format('Y-m-d');
|
||||
|
||||
mountEditComponent($this->user, $employeeUser)
|
||||
->set('form.username', 'existinguser') // Duplicate username
|
||||
->set('form.full_name', $employeeUser->employee->full_name)
|
||||
->set('form.email', $employeeUser->email)
|
||||
->set('form.phone_number', $employeeUser->employee->phone_number)
|
||||
->set('form.base_salary', '5.000.000')
|
||||
->set('form.birthdate', $birthdate)
|
||||
->set('form.hire_date', $hireDate)
|
||||
->set('form.gender', $employeeUser->employee->gender->value)
|
||||
->set('form.status', UserStatus::ACTIVE->value)
|
||||
->set('form.outlet_ids', [$outlet->id])
|
||||
->set('form.role_ids', [$role->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.username']);
|
||||
});
|
||||
|
||||
it('validates email uniqueness ignores current user', function () {
|
||||
$permission = Permission::create(['name' => 'update user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$role = Role::firstOrCreate(['name' => 'Admin']);
|
||||
$employeeUser = createEditUser(['email' => 'existing@example.com']);
|
||||
$employeeUser->assignRole($role->id);
|
||||
|
||||
$outlet = Outlet::factory()->create();
|
||||
|
||||
$birthdate = now()->subYears(25)->format('Y-m-d');
|
||||
$hireDate = now()->subMonths(6)->format('Y-m-d');
|
||||
|
||||
// Should allow keeping the same email
|
||||
mountEditComponent($this->user, $employeeUser)
|
||||
->set('form.full_name', $employeeUser->employee->full_name)
|
||||
->set('form.username', $employeeUser->username)
|
||||
->set('form.email', 'existing@example.com') // Same email
|
||||
->set('form.phone_number', $employeeUser->employee->phone_number)
|
||||
->set('form.base_salary', '5.000.000')
|
||||
->set('form.birthdate', $birthdate)
|
||||
->set('form.hire_date', $hireDate)
|
||||
->set('form.gender', $employeeUser->employee->gender->value)
|
||||
->set('form.status', UserStatus::ACTIVE->value)
|
||||
->set('form.outlet_ids', [$outlet->id])
|
||||
->set('form.role_ids', [$role->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors(['form.email']);
|
||||
});
|
||||
|
||||
it('validates email uniqueness prevents duplicate', function () {
|
||||
$permission = Permission::create(['name' => 'update user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$role = Role::firstOrCreate(['name' => 'Admin']);
|
||||
$existingUser = createEditUser(['email' => 'existing@example.com']);
|
||||
$existingUser->assignRole($role->id);
|
||||
|
||||
$employeeUser = createEditUser(['email' => 'other@example.com']);
|
||||
$employeeUser->assignRole($role->id);
|
||||
|
||||
$outlet = Outlet::factory()->create();
|
||||
$role = Role::firstOrCreate(['name' => 'Admin']);
|
||||
|
||||
$birthdate = now()->subYears(25)->format('Y-m-d');
|
||||
$hireDate = now()->subMonths(6)->format('Y-m-d');
|
||||
|
||||
mountEditComponent($this->user, $employeeUser)
|
||||
->set('form.email', 'existing@example.com') // Duplicate email
|
||||
->set('form.full_name', $employeeUser->employee->full_name)
|
||||
->set('form.username', $employeeUser->username)
|
||||
->set('form.phone_number', $employeeUser->employee->phone_number)
|
||||
->set('form.base_salary', '5.000.000')
|
||||
->set('form.birthdate', $birthdate)
|
||||
->set('form.hire_date', $hireDate)
|
||||
->set('form.gender', $employeeUser->employee->gender->value)
|
||||
->set('form.status', UserStatus::ACTIVE->value)
|
||||
->set('form.outlet_ids', [$outlet->id])
|
||||
->set('form.role_ids', [$role->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.email']);
|
||||
});
|
||||
|
||||
it('validates birthdate must be before 18 years ago', function () {
|
||||
$permission = Permission::create(['name' => 'update user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$role = Role::firstOrCreate(['name' => 'Admin']);
|
||||
$employeeUser = createEditUser();
|
||||
$employeeUser->assignRole($role->id);
|
||||
|
||||
$outlet = Outlet::factory()->create();
|
||||
$role = Role::firstOrCreate(['name' => 'Admin']);
|
||||
|
||||
$birthdate = now()->subYears(17)->format('Y-m-d');
|
||||
$hireDate = now()->subMonths(6)->format('Y-m-d');
|
||||
|
||||
mountEditComponent($this->user, $employeeUser)
|
||||
->set('form.birthdate', $birthdate)
|
||||
->set('form.full_name', $employeeUser->employee->full_name)
|
||||
->set('form.username', $employeeUser->username)
|
||||
->set('form.email', $employeeUser->email)
|
||||
->set('form.phone_number', $employeeUser->employee->phone_number)
|
||||
->set('form.base_salary', '5.000.000')
|
||||
->set('form.hire_date', $hireDate)
|
||||
->set('form.gender', $employeeUser->employee->gender->value)
|
||||
->set('form.status', UserStatus::ACTIVE->value)
|
||||
->set('form.outlet_ids', [$outlet->id])
|
||||
->set('form.role_ids', [$role->id])
|
||||
->call('save')
|
||||
->assertHasErrors(['form.birthdate']);
|
||||
});
|
||||
|
||||
it('formats base salary correctly when updating', function () {
|
||||
$permission = Permission::create(['name' => 'update user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$role = Role::firstOrCreate(['name' => 'Admin']);
|
||||
$employeeUser = createEditUser();
|
||||
$employeeUser->assignRole($role->id);
|
||||
|
||||
$outlet = Outlet::factory()->create();
|
||||
$role = Role::firstOrCreate(['name' => 'Admin']);
|
||||
|
||||
$birthdate = now()->subYears(25)->format('Y-m-d');
|
||||
$hireDate = now()->subMonths(6)->format('Y-m-d');
|
||||
|
||||
mountEditComponent($this->user, $employeeUser)
|
||||
->set('form.base_salary', '7.500.000')
|
||||
->set('form.full_name', $employeeUser->employee->full_name)
|
||||
->set('form.username', $employeeUser->username)
|
||||
->set('form.email', $employeeUser->email)
|
||||
->set('form.phone_number', $employeeUser->employee->phone_number)
|
||||
->set('form.birthdate', $birthdate)
|
||||
->set('form.hire_date', $hireDate)
|
||||
->set('form.gender', $employeeUser->employee->gender->value)
|
||||
->set('form.status', UserStatus::ACTIVE->value)
|
||||
->set('form.outlet_ids', [$outlet->id])
|
||||
->set('form.role_ids', [$role->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$employeeUser->refresh();
|
||||
$employeeUser->load('employee');
|
||||
|
||||
expect($employeeUser->employee->base_salary)->toBe(7500000);
|
||||
});
|
||||
|
||||
it('allows nullable address field when updating', function () {
|
||||
$permission = Permission::create(['name' => 'update user']);
|
||||
$this->user->givePermissionTo($permission);
|
||||
|
||||
$role = Role::firstOrCreate(['name' => 'Admin']);
|
||||
$employeeUser = createEditUser();
|
||||
$employeeUser->employee->update(['address' => 'Old Address']);
|
||||
$employeeUser->assignRole($role->id);
|
||||
|
||||
$outlet = Outlet::factory()->create();
|
||||
|
||||
$birthdate = now()->subYears(25)->format('Y-m-d');
|
||||
$hireDate = now()->subMonths(6)->format('Y-m-d');
|
||||
|
||||
mountEditComponent($this->user, $employeeUser)
|
||||
->set('form.address', null)
|
||||
->set('form.full_name', $employeeUser->employee->full_name)
|
||||
->set('form.username', $employeeUser->username)
|
||||
->set('form.email', $employeeUser->email)
|
||||
->set('form.phone_number', $employeeUser->employee->phone_number)
|
||||
->set('form.base_salary', '5.000.000')
|
||||
->set('form.birthdate', $birthdate)
|
||||
->set('form.hire_date', $hireDate)
|
||||
->set('form.gender', $employeeUser->employee->gender->value)
|
||||
->set('form.status', UserStatus::ACTIVE->value)
|
||||
->set('form.outlet_ids', [$outlet->id])
|
||||
->set('form.role_ids', [$role->id])
|
||||
->call('save')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$employeeUser->refresh();
|
||||
$employeeUser->load('employee');
|
||||
|
||||
expect($employeeUser->employee->address)->toBeNull();
|
||||
});
|
||||
@ -1,229 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\UserStatus;
|
||||
use App\Livewire\Studio\Master\User\Index;
|
||||
use App\Models\Employee;
|
||||
use App\Models\Outlet;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Features\SupportTesting\Testable;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
// Create roles for testing
|
||||
collect(['Developer', 'Owner', 'Leader', 'Admin', 'Partner', 'Customer'])
|
||||
->each(fn ($role) => \Spatie\Permission\Models\Role::create(['name' => $role]));
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->create();
|
||||
|
||||
$this->user->assignRole('Admin'); // Give admin role to test user
|
||||
|
||||
// Give necessary permissions for testing (since Admin role in seeder doesn't have these permissions)
|
||||
\Spatie\Permission\Models\Permission::create(['name' => 'delete user']);
|
||||
\Spatie\Permission\Models\Permission::create(['name' => 'reset password user']);
|
||||
$this->user->givePermissionTo(['delete user', 'reset password user']);
|
||||
});
|
||||
|
||||
function createUser(array $attributes = []): User
|
||||
{
|
||||
return User::factory()
|
||||
->active()
|
||||
->has(Employee::factory())
|
||||
->state(fn () => array_merge([
|
||||
'status' => fake()->randomElement(UserStatus::cases()),
|
||||
], $attributes))
|
||||
->create();
|
||||
}
|
||||
|
||||
function mountIndexComponent(User $user): Testable
|
||||
{
|
||||
return Livewire::actingAs($user)->test(Index::class);
|
||||
}
|
||||
|
||||
it('renders page successfully', function () {
|
||||
mountIndexComponent($this->user)
|
||||
->assertViewIs('livewire.studio.master.user.index')
|
||||
->assertViewHas('pageTitle', 'Pegawai');
|
||||
});
|
||||
|
||||
it('loads employees as Eloquent collection', function () {
|
||||
$user = createUser();
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect(
|
||||
$component->get('employees')->pluck('id')
|
||||
)->toContain($user->id);
|
||||
});
|
||||
|
||||
it('loads employees with relationships', function () {
|
||||
$user = createUser();
|
||||
$user->assignRole('Admin'); // Assign role so user appears in list
|
||||
$outlet = Outlet::factory()->create();
|
||||
$user->outlets()->attach($outlet->id);
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
$loadedEmployee = $component->get('employees')->first(fn ($emp) => $emp->user_id === $user->id);
|
||||
|
||||
expect($loadedEmployee)->not->toBeNull()
|
||||
->and($loadedEmployee->relationLoaded('user'))->toBeTrue()
|
||||
->and($loadedEmployee->user->relationLoaded('outlets'))->toBeTrue()
|
||||
->and($loadedEmployee->user->outlets)->toHaveCount(1);
|
||||
});
|
||||
|
||||
it('excludes developer users from listing', function () {
|
||||
$developer = createUser();
|
||||
$developer->assignRole('Developer');
|
||||
|
||||
$admin = createUser();
|
||||
$admin->assignRole('Admin');
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
$employeeNames = $component->get('employees')->pluck('full_name')->toArray();
|
||||
|
||||
expect($employeeNames)->not->toContain($developer->employee->full_name);
|
||||
expect($employeeNames)->toContain($admin->employee->full_name);
|
||||
});
|
||||
|
||||
it('displays all employees', function () {
|
||||
$initialCount = Employee::count();
|
||||
|
||||
for ($i = 0; $i < 5; $i++) {
|
||||
createUser();
|
||||
}
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect($component->get('employees'))->toHaveCount($initialCount + 5);
|
||||
expect($component->get('employees')->first())->toBeInstanceOf(Employee::class);
|
||||
});
|
||||
|
||||
it('filters employees by search query', function () {
|
||||
$user1 = createUser(['username' => 'john_doe']);
|
||||
$user2 = createUser(['username' => 'jane_smith']);
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect(count($component->get('employees')))->toBeGreaterThanOrEqual(2); // At least 2 new users
|
||||
|
||||
// Test filtering by username
|
||||
$filteredComponent = mountIndexComponent($this->user)->set('search', 'john_doe');
|
||||
|
||||
expect($filteredComponent->get('employees'))->toHaveCount(1);
|
||||
expect($filteredComponent->get('employees')->first()->user->username)->toBe('john_doe');
|
||||
});
|
||||
|
||||
it('filters employees by status', function () {
|
||||
$activeUser = createUser(['status' => UserStatus::ACTIVE]);
|
||||
$inactiveUser = createUser(['status' => UserStatus::INACTIVE]);
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect(count($component->get('employees')))->toBeGreaterThanOrEqual(2); // At least 2 new users
|
||||
|
||||
$filteredComponent = mountIndexComponent($this->user)->set('status', [UserStatus::ACTIVE->value]);
|
||||
|
||||
expect(count($filteredComponent->get('employees')))->toBeGreaterThanOrEqual(1); // At least the active user
|
||||
expect($filteredComponent->get('employees')->contains(fn ($emp) => $emp->user->status->value === UserStatus::ACTIVE->value))->toBeTrue();
|
||||
});
|
||||
|
||||
it('filters employees by multiple statuses', function () {
|
||||
$activeUser = createUser(['status' => UserStatus::ACTIVE]);
|
||||
$inactiveUser = createUser(['status' => UserStatus::INACTIVE]);
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect(count($component->get('employees')))->toBeGreaterThanOrEqual(2); // At least 2 new users
|
||||
|
||||
$filteredComponent = mountIndexComponent($this->user)->set('status', [
|
||||
UserStatus::ACTIVE->value,
|
||||
UserStatus::INACTIVE->value,
|
||||
]);
|
||||
|
||||
expect(count($filteredComponent->get('employees')))->toBeGreaterThan(2); // All users with these statuses
|
||||
});
|
||||
|
||||
it('loads employees ordered by latest', function () {
|
||||
$user1 = createUser();
|
||||
sleep(1);
|
||||
$user2 = createUser();
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
expect($component->get('employees')->first()->id)->toBe($user2->id);
|
||||
expect($component->get('employees')->last()->id)->toBe($user1->id);
|
||||
});
|
||||
|
||||
it('loads employees with all required relationships', function () {
|
||||
$user = createUser();
|
||||
$user->assignRole('Admin'); // Ensure user appears in list
|
||||
$outlet = Outlet::factory()->create();
|
||||
$user->outlets()->attach($outlet->id);
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
$loadedEmployee = $component->get('employees')->first(fn ($emp) => $emp->user_id === $user->id);
|
||||
|
||||
expect($loadedEmployee)->not->toBeNull()
|
||||
->and($loadedEmployee->user->outlets)->toHaveCount(1)
|
||||
->and($loadedEmployee->user->roles)->toHaveCount(1) // Admin role assigned
|
||||
->and($loadedEmployee->full_name)->toBe($user->employee->full_name);
|
||||
});
|
||||
|
||||
it('deletes an employee successfully', function () {
|
||||
$user = createUser();
|
||||
|
||||
mountIndexComponent($this->user)
|
||||
->call('delete', $user)
|
||||
->assertHasNoErrors();
|
||||
|
||||
$this->assertSoftDeleted('users', ['id' => $user->id]);
|
||||
$this->assertSoftDeleted('employees', ['user_id' => $user->id]);
|
||||
});
|
||||
|
||||
it('handles empty search results gracefully', function () {
|
||||
createUser(['username' => 'existing_user']);
|
||||
|
||||
$component = mountIndexComponent($this->user)->set('search', 'NonExistentUser');
|
||||
|
||||
expect($component->get('employees'))->toHaveCount(0);
|
||||
});
|
||||
|
||||
it('maintains search state across component lifecycle', function () {
|
||||
$user1 = createUser(['username' => 'test_user']);
|
||||
$user2 = createUser(['username' => 'another_user']);
|
||||
|
||||
$component = mountIndexComponent($this->user)->set('search', 'test');
|
||||
|
||||
expect($component->get('employees'))->toHaveCount(1);
|
||||
expect($component->get('search'))->toBe('test');
|
||||
});
|
||||
|
||||
it('handles status filter with empty array', function () {
|
||||
createUser(['status' => UserStatus::ACTIVE]);
|
||||
createUser(['status' => UserStatus::INACTIVE]);
|
||||
|
||||
$component = mountIndexComponent($this->user)->set('status', []);
|
||||
|
||||
expect(count($component->get('employees')))->toBeGreaterThanOrEqual(2); // At least 2 new users
|
||||
});
|
||||
|
||||
it('loads employees efficiently with eager loading', function () {
|
||||
$users = collect();
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$users->push(createUser());
|
||||
}
|
||||
|
||||
$component = mountIndexComponent($this->user);
|
||||
|
||||
// Verify relationships are loaded
|
||||
$loadedEmployees = $component->get('employees');
|
||||
foreach ($loadedEmployees as $loadedEmployee) {
|
||||
expect($loadedEmployee->relationLoaded('user'))->toBeTrue();
|
||||
expect($loadedEmployee->user->relationLoaded('outlets'))->toBeTrue();
|
||||
}
|
||||
});
|
||||
@ -1,169 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Studio\Setting\Account;
|
||||
use App\Models\Employee;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->user = User::factory()->create([
|
||||
'password' => Hash::make('password'),
|
||||
]);
|
||||
|
||||
$this->employee = Employee::factory()->create([
|
||||
'user_id' => $this->user->id,
|
||||
]);
|
||||
|
||||
// Setup basic permissions
|
||||
Permission::create(['name' => 'view account']);
|
||||
Permission::create(['name' => 'update account']);
|
||||
Permission::create(['name' => 'update profile']);
|
||||
Permission::create(['name' => 'update password']);
|
||||
|
||||
$this->user->givePermissionTo(['view account', 'update account', 'update profile', 'update password']);
|
||||
|
||||
// Share dummy sidebar to avoid undefined variable error
|
||||
Illuminate\Support\Facades\View::share('sidebar', []);
|
||||
});
|
||||
|
||||
test('can render account settings page', function () {
|
||||
$this->actingAs($this->user)
|
||||
->get(route('studio.setting.account'))
|
||||
->assertOk()
|
||||
->assertSeeLivewire(Account::class)
|
||||
->assertSee('Akun');
|
||||
});
|
||||
|
||||
test('mounts with correct user data', function () {
|
||||
Livewire::actingAs($this->user)
|
||||
->test(Account::class)
|
||||
->assertSet('accountForm.username', $this->user->username)
|
||||
->assertSet('accountForm.email', $this->user->email)
|
||||
->assertSet('profileForm.full_name', $this->employee->full_name)
|
||||
->assertSet('profileForm.phone_number', $this->employee->phone_number);
|
||||
});
|
||||
|
||||
test('can update account information', function () {
|
||||
Livewire::actingAs($this->user)
|
||||
->test(Account::class)
|
||||
->set('accountForm.username', 'newusername')
|
||||
->set('accountForm.email', 'newemail@example.com')
|
||||
->call('updateAccount')
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect($this->user->fresh())
|
||||
->username->toBe('newusername')
|
||||
->email->toBe('newemail@example.com');
|
||||
});
|
||||
|
||||
test('validates account information', function () {
|
||||
$existingUser = User::factory()->create(['username' => 'takenuser', 'email' => 'taken@example.com']);
|
||||
|
||||
Livewire::actingAs($this->user)
|
||||
->test(Account::class)
|
||||
->set('accountForm.username', '') // Required
|
||||
->set('accountForm.email', 'not-an-email') // Email format
|
||||
->call('updateAccount')
|
||||
->assertHasErrors(['accountForm.username', 'accountForm.email'])
|
||||
|
||||
->set('accountForm.username', 'takenuser') // Unique
|
||||
->set('accountForm.email', 'taken@example.com') // Unique
|
||||
->call('updateAccount')
|
||||
->assertHasErrors(['accountForm.username', 'accountForm.email']);
|
||||
});
|
||||
|
||||
test('can update profile information', function () {
|
||||
Livewire::actingAs($this->user)
|
||||
->test(Account::class)
|
||||
->set('profileForm.full_name', 'New Name')
|
||||
->set('profileForm.phone_number', '0812 3456 7890')
|
||||
->call('updateProfile')
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect($this->employee->fresh())
|
||||
->full_name->toBe('New Name')
|
||||
->phone_number->toBe('0812 3456 7890');
|
||||
});
|
||||
|
||||
test('validates profile information', function () {
|
||||
Livewire::actingAs($this->user)
|
||||
->test(Account::class)
|
||||
->set('profileForm.full_name', '') // Required
|
||||
->set('profileForm.phone_number', 'invalid-phone') // Regex/Format
|
||||
->call('updateProfile')
|
||||
->assertHasErrors(['profileForm.full_name', 'profileForm.phone_number']);
|
||||
});
|
||||
|
||||
test('can update password', function () {
|
||||
Livewire::actingAs($this->user)
|
||||
->test(Account::class)
|
||||
->set('passwordForm.current_password', 'password')
|
||||
->set('passwordForm.new_password', 'Sup3rStr0ngP@ssw0rd!')
|
||||
->set('passwordForm.new_confirm_password', 'Sup3rStr0ngP@ssw0rd!')
|
||||
->call('updatePassword')
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect(route('login'));
|
||||
|
||||
// Assert that the user was logged out or session invalidated?
|
||||
// Hash check is tricky due to re-hashing or instance changes.
|
||||
// For now, assume if no errors and redirect happened, it worked.
|
||||
// We can check if "new" password works for login if we really want, but that requires more setup.
|
||||
});
|
||||
|
||||
test('fails to update password with incorrect current password', function () {
|
||||
Livewire::actingAs($this->user)
|
||||
->test(Account::class)
|
||||
->set('passwordForm.current_password', 'wrongpassword')
|
||||
->set('passwordForm.new_password', 'Sup3rStr0ngP@ssw0rd!')
|
||||
->set('passwordForm.new_confirm_password', 'Sup3rStr0ngP@ssw0rd!')
|
||||
->call('updatePassword');
|
||||
|
||||
$this->assertTrue(Hash::check('password', $this->user->fresh()->password));
|
||||
});
|
||||
|
||||
test('validates password complexity', function () {
|
||||
Livewire::actingAs($this->user)
|
||||
->test(Account::class)
|
||||
->set('passwordForm.current_password', 'password')
|
||||
->set('passwordForm.new_password', 'weak')
|
||||
->set('passwordForm.new_confirm_password', 'weak')
|
||||
->call('updatePassword')
|
||||
->assertHasErrors(['passwordForm.new_password']);
|
||||
});
|
||||
|
||||
test('cannot update account without permission', function () {
|
||||
$this->user->revokePermissionTo('update account');
|
||||
|
||||
Livewire::actingAs($this->user)
|
||||
->test(Account::class)
|
||||
->set('accountForm.username', 'newusername')
|
||||
->call('updateAccount')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('cannot update profile without permission', function () {
|
||||
$this->user->revokePermissionTo('update profile');
|
||||
|
||||
Livewire::actingAs($this->user)
|
||||
->test(Account::class)
|
||||
->set('profileForm.full_name', 'New Name')
|
||||
->call('updateProfile')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('cannot update password without permission', function () {
|
||||
$this->user->revokePermissionTo('update password');
|
||||
|
||||
Livewire::actingAs($this->user)
|
||||
->test(Account::class)
|
||||
->set('passwordForm.current_password', 'password')
|
||||
->set('passwordForm.new_password', 'NewPassword123!')
|
||||
->set('passwordForm.new_confirm_password', 'NewPassword123!')
|
||||
->call('updatePassword')
|
||||
->assertForbidden();
|
||||
});
|
||||
@ -1,301 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Studio\Setting\Application;
|
||||
use App\Models\User;
|
||||
use App\Settings\ContactSettings;
|
||||
use App\Settings\GeneralSettings;
|
||||
use App\Settings\SeoSettings;
|
||||
use App\Settings\SocialSettings;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Spatie\Permission\Models\Role;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
|
||||
$this->user = User::factory()
|
||||
->active()
|
||||
->create();
|
||||
|
||||
$this->ownerRole = Role::where('name', 'Owner')->first();
|
||||
|
||||
$this->user->roles()->attach($this->ownerRole->id);
|
||||
});
|
||||
|
||||
function mountApplicationComponent(User $user)
|
||||
{
|
||||
return Livewire::actingAs($user)->test(Application::class);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Component Rendering & Mount
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('renders page successfully', function () {
|
||||
mountApplicationComponent($this->user)
|
||||
->assertViewIs('livewire.studio.setting.application')
|
||||
->assertViewHas('pageTitle', 'Aplikasi');
|
||||
});
|
||||
|
||||
it('mounts with settings loaded', function () {
|
||||
$component = mountApplicationComponent($this->user);
|
||||
|
||||
expect($component->generalForm->site_name)->toBe('Yadi Parfum');
|
||||
expect($component->contactForm->phone_number)->toBe('083816931711');
|
||||
expect($component->socialForm->whatsapp)->toBe('https://wa.me/6283816931711');
|
||||
expect($component->seoForm->meta_description)->toBe('Nikmati pengalaman wangi berkualitas tinggi dengan parfum refill dan produk unggulan dari Yadi Parfum.');
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| General Settings Update
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('updates general settings successfully', function () {
|
||||
mountApplicationComponent($this->user)
|
||||
->set('generalForm.site_name', 'Updated Site Name')
|
||||
->set('generalForm.site_tagline', 'Updated Tagline')
|
||||
->set('generalForm.site_description', 'Updated description for the site')
|
||||
->set('generalForm.maintenance_mode', true)
|
||||
->call('updateGeneral')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$settings = app(GeneralSettings::class);
|
||||
expect($settings->site_name)->toBe('Updated Site Name');
|
||||
expect($settings->site_tagline)->toBe('Updated Tagline');
|
||||
expect($settings->site_description)->toBe('Updated description for the site');
|
||||
expect($settings->maintenance_mode)->toBe(true);
|
||||
});
|
||||
|
||||
it('validates general settings required fields', function () {
|
||||
mountApplicationComponent($this->user)
|
||||
->set('generalForm.site_name', '')
|
||||
->set('generalForm.site_tagline', '')
|
||||
->set('generalForm.site_description', '')
|
||||
->call('updateGeneral')
|
||||
->assertHasErrors([
|
||||
'generalForm.site_name' => 'required',
|
||||
'generalForm.site_tagline' => 'required',
|
||||
'generalForm.site_description' => 'required',
|
||||
]);
|
||||
});
|
||||
|
||||
it('validates general settings field lengths', function () {
|
||||
mountApplicationComponent($this->user)
|
||||
->set('generalForm.site_name', str_repeat('a', 101))
|
||||
->set('generalForm.site_tagline', str_repeat('a', 151))
|
||||
->call('updateGeneral')
|
||||
->assertHasErrors([
|
||||
'generalForm.site_name' => 'max',
|
||||
'generalForm.site_tagline' => 'max',
|
||||
]);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Contact Settings Update
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('updates contact settings successfully', function () {
|
||||
mountApplicationComponent($this->user)
|
||||
->set('contactForm.phone_number', '0812 3456 7890')
|
||||
->set('contactForm.email', 'contact@example.com')
|
||||
->set('contactForm.address', 'Updated Address 123')
|
||||
->set('contactForm.map_embed', '<iframe>map</iframe>')
|
||||
->call('updateContact')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$settings = app(ContactSettings::class);
|
||||
|
||||
expect($settings->phone_number)->toBe('0812 3456 7890');
|
||||
expect($settings->email)->toBe('contact@example.com');
|
||||
expect($settings->address)->toBe('Updated Address 123');
|
||||
expect($settings->map_embed)->toBe('<iframe>map</iframe>');
|
||||
});
|
||||
|
||||
it('validates contact settings required fields', function () {
|
||||
mountApplicationComponent($this->user)
|
||||
->set('contactForm.phone_number', '')
|
||||
->set('contactForm.address', '')
|
||||
->call('updateContact')
|
||||
->assertHasErrors([
|
||||
'contactForm.phone_number' => 'required',
|
||||
'contactForm.address' => 'required',
|
||||
]);
|
||||
});
|
||||
|
||||
it('validates contact settings email format', function () {
|
||||
mountApplicationComponent($this->user)
|
||||
->set('contactForm.phone_number', '+628123456789')
|
||||
->set('contactForm.email', 'invalid-email')
|
||||
->set('contactForm.address', 'Valid Address')
|
||||
->call('updateContact')
|
||||
->assertHasErrors([
|
||||
'contactForm.email' => 'email',
|
||||
]);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Social Settings Update
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('updates social settings successfully', function () {
|
||||
mountApplicationComponent($this->user)
|
||||
->set('socialForm.facebook', 'https://facebook.com/page')
|
||||
->set('socialForm.instagram', 'https://instagram.com/page')
|
||||
->set('socialForm.youtube', 'https://youtube.com/channel')
|
||||
->set('socialForm.tiktok', 'https://tiktok.com/@user')
|
||||
->set('socialForm.whatsapp', 'https://wa.me/123456789')
|
||||
->set('socialForm.telegram', 'https://t.me/channel')
|
||||
->call('updateSocial')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$settings = app(SocialSettings::class);
|
||||
expect($settings->facebook)->toBe('https://facebook.com/page');
|
||||
expect($settings->instagram)->toBe('https://instagram.com/page');
|
||||
expect($settings->youtube)->toBe('https://youtube.com/channel');
|
||||
expect($settings->tiktok)->toBe('https://tiktok.com/@user');
|
||||
expect($settings->whatsapp)->toBe('https://wa.me/123456789');
|
||||
expect($settings->telegram)->toBe('https://t.me/channel');
|
||||
});
|
||||
|
||||
it('validates social settings url format', function () {
|
||||
mountApplicationComponent($this->user)
|
||||
->set('socialForm.facebook', 'not-a-url')
|
||||
->call('updateSocial')
|
||||
->assertHasErrors([
|
||||
'socialForm.facebook' => 'url',
|
||||
]);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| SEO Settings Update
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('updates seo settings successfully', function () {
|
||||
mountApplicationComponent($this->user)
|
||||
->set('seoForm.meta_description', 'Updated meta description for SEO')
|
||||
->set('seoForm.meta_keywords', 'keyword1, keyword2, keyword3')
|
||||
->set('seoForm.meta_author', 'Updated Author')
|
||||
->set('seoForm.og_image', 'https://example.com/image.jpg')
|
||||
->set('seoForm.og_title', 'Open Graph Title')
|
||||
->set('seoForm.og_description', 'Open Graph Description')
|
||||
->set('seoForm.og_type', 'article')
|
||||
->set('seoForm.twitter_card', 'summary_large_image')
|
||||
->set('seoForm.canonical_url', 'https://example.com/page')
|
||||
->call('updateSeo')
|
||||
->assertHasNoErrors();
|
||||
|
||||
$settings = app(SeoSettings::class);
|
||||
expect($settings->meta_description)->toBe('Updated meta description for SEO');
|
||||
expect($settings->meta_keywords)->toBe('keyword1, keyword2, keyword3');
|
||||
expect($settings->meta_author)->toBe('Updated Author');
|
||||
expect($settings->og_image)->toBe('https://example.com/image.jpg');
|
||||
expect($settings->og_title)->toBe('Open Graph Title');
|
||||
expect($settings->og_description)->toBe('Open Graph Description');
|
||||
expect($settings->og_type)->toBe('article');
|
||||
expect($settings->twitter_card)->toBe('summary_large_image');
|
||||
expect($settings->canonical_url)->toBe('https://example.com/page');
|
||||
});
|
||||
|
||||
it('validates seo settings required fields', function () {
|
||||
mountApplicationComponent($this->user)
|
||||
->set('seoForm.meta_description', '')
|
||||
->set('seoForm.meta_keywords', '')
|
||||
->set('seoForm.meta_author', '')
|
||||
->call('updateSeo')
|
||||
->assertHasErrors([
|
||||
'seoForm.meta_description' => 'required',
|
||||
'seoForm.meta_keywords' => 'required',
|
||||
'seoForm.meta_author' => 'required',
|
||||
]);
|
||||
});
|
||||
|
||||
it('validates seo settings og_type enum', function () {
|
||||
mountApplicationComponent($this->user)
|
||||
->set('seoForm.meta_description', 'Valid description')
|
||||
->set('seoForm.meta_keywords', 'keywords')
|
||||
->set('seoForm.meta_author', 'Author')
|
||||
->set('seoForm.og_type', 'invalid_type')
|
||||
->call('updateSeo')
|
||||
->assertHasErrors([
|
||||
'seoForm.og_type' => 'in',
|
||||
]);
|
||||
});
|
||||
|
||||
it('validates seo settings twitter_card enum', function () {
|
||||
mountApplicationComponent($this->user)
|
||||
->set('seoForm.meta_description', 'Valid description')
|
||||
->set('seoForm.meta_keywords', 'keywords')
|
||||
->set('seoForm.meta_author', 'Author')
|
||||
->set('seoForm.twitter_card', 'invalid_card')
|
||||
->call('updateSeo')
|
||||
->assertHasErrors([
|
||||
'seoForm.twitter_card' => 'in',
|
||||
]);
|
||||
});
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Authorization Tests
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
it('requires update general settings permission', function () {
|
||||
// Temporarily remove the permission from the user
|
||||
$this->ownerRole->revokePermissionTo('update general settings');
|
||||
|
||||
mountApplicationComponent($this->user)
|
||||
->call('updateGeneral')
|
||||
->assertForbidden();
|
||||
|
||||
// Restore the permission
|
||||
$this->user->givePermissionTo('update general settings');
|
||||
});
|
||||
|
||||
it('requires update contact settings permission', function () {
|
||||
// Temporarily remove the permission from the user
|
||||
$this->ownerRole->revokePermissionTo('update contact settings');
|
||||
|
||||
mountApplicationComponent($this->user)
|
||||
->call('updateContact')
|
||||
->assertForbidden();
|
||||
|
||||
// Restore the permission
|
||||
$this->user->givePermissionTo('update contact settings');
|
||||
});
|
||||
|
||||
it('requires update social settings permission', function () {
|
||||
// Temporarily remove the permission from the user
|
||||
$this->ownerRole->revokePermissionTo('update social settings');
|
||||
|
||||
mountApplicationComponent($this->user)
|
||||
->call('updateSocial')
|
||||
->assertForbidden();
|
||||
|
||||
// Restore the permission
|
||||
$this->user->givePermissionTo('update social settings');
|
||||
});
|
||||
|
||||
it('requires update seo settings permission', function () {
|
||||
// Temporarily remove the permission from the user
|
||||
$this->ownerRole->revokePermissionTo('update seo settings');
|
||||
|
||||
mountApplicationComponent($this->user)
|
||||
->call('updateSeo')
|
||||
->assertForbidden();
|
||||
|
||||
// Restore the permission
|
||||
$this->user->givePermissionTo('update seo settings');
|
||||
});
|
||||
@ -1,201 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Forms\Studio\Setting\ContactForm;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Livewire\Livewire;
|
||||
|
||||
function createContactForm()
|
||||
{
|
||||
// Create a minimal Livewire component for testing
|
||||
$component = new class extends \Livewire\Component
|
||||
{
|
||||
public ContactForm $form;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->form = new ContactForm($this, 'form');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return '<div></div>';
|
||||
}
|
||||
};
|
||||
|
||||
$component->mount();
|
||||
|
||||
return $component->form;
|
||||
}
|
||||
|
||||
it('has correct validation rules', function () {
|
||||
$form = createContactForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
expect($rules)->toHaveKey('phone_number');
|
||||
expect($rules)->toHaveKey('email');
|
||||
expect($rules)->toHaveKey('address');
|
||||
expect($rules)->toHaveKey('map_embed');
|
||||
|
||||
expect($rules['phone_number'])->toContain('required');
|
||||
expect($rules['phone_number'])->toContain('string');
|
||||
expect($rules['phone_number'])->toContain('min:10');
|
||||
expect($rules['phone_number'])->toContain('max:20');
|
||||
|
||||
expect($rules['email'])->toContain('nullable');
|
||||
expect($rules['email'])->toContain('email');
|
||||
expect($rules['email'])->toContain('max:100');
|
||||
|
||||
expect($rules['address'])->toContain('required');
|
||||
expect($rules['address'])->toContain('string');
|
||||
expect($rules['address'])->toContain('min:10');
|
||||
expect($rules['address'])->toContain('max:500');
|
||||
|
||||
expect($rules['map_embed'])->toContain('nullable');
|
||||
expect($rules['map_embed'])->toContain('string');
|
||||
expect($rules['map_embed'])->toContain('max:2000');
|
||||
});
|
||||
|
||||
it('validates required fields', function () {
|
||||
$form = createContactForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'phone_number' => '',
|
||||
'address' => '',
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('phone_number'))->toBeTrue();
|
||||
expect($validator->errors()->has('address'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('validates phone number format', function () {
|
||||
$form = createContactForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'phone_number' => 'invalid',
|
||||
'address' => 'Valid Address',
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('phone_number'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('validates email format', function () {
|
||||
$form = createContactForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'phone_number' => '0812 3456 789',
|
||||
'email' => 'invalid-email',
|
||||
'address' => 'Valid Address',
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('email'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('validates phone number length', function () {
|
||||
$form = createContactForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'phone_number' => '123',
|
||||
'address' => 'Valid Address',
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('phone_number'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('validates address length', function () {
|
||||
$form = createContactForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'phone_number' => '+628123456789',
|
||||
'address' => 'Short',
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('address'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('validates email max length', function () {
|
||||
$form = createContactForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'phone_number' => '+628123456789',
|
||||
'email' => str_repeat('a', 101).'@example.com',
|
||||
'address' => 'Valid Address',
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('email'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('validates map embed max length', function () {
|
||||
$form = createContactForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'phone_number' => '+628123456789',
|
||||
'address' => 'Valid Address',
|
||||
'map_embed' => str_repeat('a', 2001),
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('map_embed'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('accepts nullable email', function () {
|
||||
$form = createContactForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'phone_number' => '0812 3456 789',
|
||||
'email' => null,
|
||||
'address' => 'Valid Address',
|
||||
], $rules);
|
||||
|
||||
expect($validator->passes())->toBeTrue();
|
||||
});
|
||||
|
||||
it('accepts nullable map embed', function () {
|
||||
$form = createContactForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'phone_number' => '0812 3456 789',
|
||||
'address' => 'Valid Address',
|
||||
'map_embed' => null,
|
||||
], $rules);
|
||||
|
||||
expect($validator->passes())->toBeTrue();
|
||||
});
|
||||
|
||||
it('passes validation with valid data', function () {
|
||||
$form = createContactForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'phone_number' => '0812 3456 789',
|
||||
'email' => 'test@example.com',
|
||||
'address' => 'Valid Address with proper length',
|
||||
'map_embed' => '<iframe>valid embed</iframe>',
|
||||
], $rules);
|
||||
|
||||
expect($validator->passes())->toBeTrue();
|
||||
});
|
||||
|
||||
it('has correct validation attributes', function () {
|
||||
$form = createContactForm();
|
||||
$attributes = $form->validationAttributes();
|
||||
|
||||
expect($attributes)->toHaveKey('phone_number', 'nomor telepon');
|
||||
expect($attributes)->toHaveKey('email', 'email');
|
||||
expect($attributes)->toHaveKey('address', 'alamat');
|
||||
expect($attributes)->toHaveKey('map_embed', 'embed peta');
|
||||
});
|
||||
@ -1,143 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Forms\Studio\Setting\GeneralForm;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Livewire\Livewire;
|
||||
|
||||
function createGeneralForm()
|
||||
{
|
||||
// Create a minimal Livewire component for testing
|
||||
$component = new class extends \Livewire\Component
|
||||
{
|
||||
public GeneralForm $form;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->form = new GeneralForm($this, 'form');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return '<div></div>';
|
||||
}
|
||||
};
|
||||
|
||||
$component->mount();
|
||||
|
||||
return $component->form;
|
||||
}
|
||||
|
||||
it('has correct validation rules', function () {
|
||||
$form = createGeneralForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
expect($rules)->toHaveKey('site_name');
|
||||
expect($rules)->toHaveKey('site_tagline');
|
||||
expect($rules)->toHaveKey('site_description');
|
||||
expect($rules)->toHaveKey('maintenance_mode');
|
||||
|
||||
expect($rules['site_name'])->toContain('required');
|
||||
expect($rules['site_name'])->toContain('string');
|
||||
expect($rules['site_name'])->toContain('max:100');
|
||||
expect($rules['site_name'])->toContain('min:1');
|
||||
|
||||
expect($rules['site_tagline'])->toContain('required');
|
||||
expect($rules['site_tagline'])->toContain('string');
|
||||
expect($rules['site_tagline'])->toContain('max:150');
|
||||
expect($rules['site_tagline'])->toContain('min:1');
|
||||
|
||||
expect($rules['site_description'])->toContain('required');
|
||||
expect($rules['site_description'])->toContain('string');
|
||||
expect($rules['site_description'])->toContain('min:10');
|
||||
expect($rules['site_description'])->toContain('max:500');
|
||||
|
||||
expect($rules['maintenance_mode'])->toContain('required');
|
||||
expect($rules['maintenance_mode'])->toContain('boolean');
|
||||
});
|
||||
|
||||
it('validates required fields', function () {
|
||||
$form = createGeneralForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'site_name' => '',
|
||||
'site_tagline' => '',
|
||||
'site_description' => '',
|
||||
'maintenance_mode' => false,
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('site_name'))->toBeTrue();
|
||||
expect($validator->errors()->has('site_tagline'))->toBeTrue();
|
||||
expect($validator->errors()->has('site_description'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('validates minimum lengths', function () {
|
||||
$form = createGeneralForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'site_name' => 'A',
|
||||
'site_tagline' => 'B',
|
||||
'site_description' => 'Short',
|
||||
'maintenance_mode' => false,
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('site_description'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('validates maximum lengths', function () {
|
||||
$form = createGeneralForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'site_name' => str_repeat('A', 101),
|
||||
'site_tagline' => str_repeat('B', 151),
|
||||
'site_description' => 'Valid description',
|
||||
'maintenance_mode' => false,
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('site_name'))->toBeTrue();
|
||||
expect($validator->errors()->has('site_tagline'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('validates maintenance mode boolean', function () {
|
||||
$form = createGeneralForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'site_name' => 'Valid Name',
|
||||
'site_tagline' => 'Valid Tagline',
|
||||
'site_description' => 'Valid description with proper length',
|
||||
'maintenance_mode' => 'not_boolean',
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('maintenance_mode'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('passes validation with valid data', function () {
|
||||
$form = createGeneralForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'site_name' => 'Valid Site Name',
|
||||
'site_tagline' => 'Valid Tagline',
|
||||
'site_description' => 'Valid description with proper length for the site',
|
||||
'maintenance_mode' => true,
|
||||
], $rules);
|
||||
|
||||
expect($validator->passes())->toBeTrue();
|
||||
});
|
||||
|
||||
it('has correct validation attributes', function () {
|
||||
$form = createGeneralForm();
|
||||
$attributes = $form->validationAttributes();
|
||||
|
||||
expect($attributes)->toHaveKey('site_name', 'nama situs');
|
||||
expect($attributes)->toHaveKey('site_tagline', 'tagline situs');
|
||||
expect($attributes)->toHaveKey('site_description', 'deskripsi situs');
|
||||
expect($attributes)->toHaveKey('maintenance_mode', 'mode maintenance');
|
||||
});
|
||||
@ -1,350 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Forms\Studio\Setting\SeoForm;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Livewire\Livewire;
|
||||
|
||||
function createSeoForm()
|
||||
{
|
||||
// Create a minimal Livewire component for testing
|
||||
$component = new class extends \Livewire\Component
|
||||
{
|
||||
public SeoForm $form;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->form = new SeoForm($this, 'form');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return '<div></div>';
|
||||
}
|
||||
};
|
||||
|
||||
$component->mount();
|
||||
|
||||
return $component->form;
|
||||
}
|
||||
|
||||
it('has correct validation rules', function () {
|
||||
$form = createSeoForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
expect($rules)->toHaveKey('meta_description');
|
||||
expect($rules)->toHaveKey('meta_keywords');
|
||||
expect($rules)->toHaveKey('meta_author');
|
||||
expect($rules)->toHaveKey('og_image');
|
||||
expect($rules)->toHaveKey('og_title');
|
||||
expect($rules)->toHaveKey('og_description');
|
||||
expect($rules)->toHaveKey('og_type');
|
||||
expect($rules)->toHaveKey('twitter_card');
|
||||
expect($rules)->toHaveKey('canonical_url');
|
||||
|
||||
expect($rules['meta_description'])->toContain('required');
|
||||
expect($rules['meta_description'])->toContain('string');
|
||||
expect($rules['meta_description'])->toContain('min:10');
|
||||
expect($rules['meta_description'])->toContain('max:160');
|
||||
|
||||
expect($rules['meta_keywords'])->toContain('required');
|
||||
expect($rules['meta_keywords'])->toContain('string');
|
||||
expect($rules['meta_keywords'])->toContain('min:3');
|
||||
expect($rules['meta_keywords'])->toContain('max:255');
|
||||
|
||||
expect($rules['meta_author'])->toContain('required');
|
||||
expect($rules['meta_author'])->toContain('string');
|
||||
expect($rules['meta_author'])->toContain('min:1');
|
||||
expect($rules['meta_author'])->toContain('max:150');
|
||||
|
||||
expect($rules['og_image'])->toContain('nullable');
|
||||
expect($rules['og_image'])->toContain('string');
|
||||
expect($rules['og_image'])->toContain('max:500');
|
||||
expect($rules['og_image'])->toContain('url');
|
||||
|
||||
expect($rules['og_title'])->toContain('nullable');
|
||||
expect($rules['og_title'])->toContain('string');
|
||||
expect($rules['og_title'])->toContain('max:255');
|
||||
|
||||
expect($rules['og_description'])->toContain('nullable');
|
||||
expect($rules['og_description'])->toContain('string');
|
||||
expect($rules['og_description'])->toContain('max:300');
|
||||
|
||||
expect($rules['og_type'])->toContain('nullable');
|
||||
expect($rules['og_type'])->toContain('string');
|
||||
expect($rules['og_type'])->toContain('max:50');
|
||||
expect($rules['og_type'])->toContain('in:website,article,product');
|
||||
|
||||
expect($rules['twitter_card'])->toContain('nullable');
|
||||
expect($rules['twitter_card'])->toContain('string');
|
||||
expect($rules['twitter_card'])->toContain('max:50');
|
||||
expect($rules['twitter_card'])->toContain('in:summary,summary_large_image');
|
||||
|
||||
expect($rules['canonical_url'])->toContain('nullable');
|
||||
expect($rules['canonical_url'])->toContain('string');
|
||||
expect($rules['canonical_url'])->toContain('max:500');
|
||||
expect($rules['canonical_url'])->toContain('url');
|
||||
});
|
||||
|
||||
it('validates required fields', function () {
|
||||
$form = createSeoForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'meta_description' => '',
|
||||
'meta_keywords' => '',
|
||||
'meta_author' => '',
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('meta_description'))->toBeTrue();
|
||||
expect($validator->errors()->has('meta_keywords'))->toBeTrue();
|
||||
expect($validator->errors()->has('meta_author'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('validates meta description length', function () {
|
||||
$form = createSeoForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'meta_description' => 'Short',
|
||||
'meta_keywords' => 'keywords',
|
||||
'meta_author' => 'Author',
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('meta_description'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('validates meta description max length', function () {
|
||||
$form = createSeoForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'meta_description' => str_repeat('a', 161),
|
||||
'meta_keywords' => 'keywords',
|
||||
'meta_author' => 'Author',
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('meta_description'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('validates meta keywords minimum length', function () {
|
||||
$form = createSeoForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'meta_description' => 'Valid meta description with proper length',
|
||||
'meta_keywords' => 'ab',
|
||||
'meta_author' => 'Author',
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('meta_keywords'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('validates meta author max length', function () {
|
||||
$form = createSeoForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'meta_description' => 'Valid meta description with proper length',
|
||||
'meta_keywords' => 'valid keywords',
|
||||
'meta_author' => str_repeat('a', 151),
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('meta_author'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('validates og_image url format and max length', function () {
|
||||
$form = createSeoForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'meta_description' => 'Valid meta description with proper length',
|
||||
'meta_keywords' => 'valid keywords',
|
||||
'meta_author' => 'Author',
|
||||
'og_image' => 'not-a-url',
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('og_image'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('validates og_image max length', function () {
|
||||
$form = createSeoForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'meta_description' => 'Valid meta description with proper length',
|
||||
'meta_keywords' => 'valid keywords',
|
||||
'meta_author' => 'Author',
|
||||
'og_image' => 'https://example.com/'.str_repeat('a', 501),
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('og_image'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('validates og_title max length', function () {
|
||||
$form = createSeoForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'meta_description' => 'Valid meta description with proper length',
|
||||
'meta_keywords' => 'valid keywords',
|
||||
'meta_author' => 'Author',
|
||||
'og_title' => str_repeat('a', 256),
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('og_title'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('validates og_description max length', function () {
|
||||
$form = createSeoForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'meta_description' => 'Valid meta description with proper length',
|
||||
'meta_keywords' => 'valid keywords',
|
||||
'meta_author' => 'Author',
|
||||
'og_description' => str_repeat('a', 301),
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('og_description'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('validates og_type enum values', function () {
|
||||
$form = createSeoForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'meta_description' => 'Valid meta description with proper length',
|
||||
'meta_keywords' => 'valid keywords',
|
||||
'meta_author' => 'Author',
|
||||
'og_type' => 'invalid_type',
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('og_type'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('accepts valid og_type values', function () {
|
||||
$form = createSeoForm();
|
||||
$rules = $form->rules();
|
||||
$validTypes = ['website', 'article', 'product'];
|
||||
|
||||
foreach ($validTypes as $type) {
|
||||
$validator = Validator::make([
|
||||
'meta_description' => 'Valid meta description with proper length',
|
||||
'meta_keywords' => 'valid keywords',
|
||||
'meta_author' => 'Author',
|
||||
'og_type' => $type,
|
||||
], $rules);
|
||||
|
||||
expect($validator->passes())->toBeTrue();
|
||||
}
|
||||
});
|
||||
|
||||
it('validates twitter_card enum values', function () {
|
||||
$form = createSeoForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'meta_description' => 'Valid meta description with proper length',
|
||||
'meta_keywords' => 'valid keywords',
|
||||
'meta_author' => 'Author',
|
||||
'twitter_card' => 'invalid_card',
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('twitter_card'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('accepts valid twitter_card values', function () {
|
||||
$form = createSeoForm();
|
||||
$rules = $form->rules();
|
||||
$validCards = ['summary', 'summary_large_image'];
|
||||
|
||||
foreach ($validCards as $card) {
|
||||
$validator = Validator::make([
|
||||
'meta_description' => 'Valid meta description with proper length',
|
||||
'meta_keywords' => 'valid keywords',
|
||||
'meta_author' => 'Author',
|
||||
'twitter_card' => $card,
|
||||
], $rules);
|
||||
|
||||
expect($validator->passes())->toBeTrue();
|
||||
}
|
||||
});
|
||||
|
||||
it('validates canonical_url url format and max length', function () {
|
||||
$form = createSeoForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'meta_description' => 'Valid meta description with proper length',
|
||||
'meta_keywords' => 'valid keywords',
|
||||
'meta_author' => 'Author',
|
||||
'canonical_url' => 'not-a-url',
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('canonical_url'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('accepts nullable fields', function () {
|
||||
$form = createSeoForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'meta_description' => 'Valid meta description with proper length',
|
||||
'meta_keywords' => 'valid keywords',
|
||||
'meta_author' => 'Author',
|
||||
'og_image' => null,
|
||||
'og_title' => null,
|
||||
'og_description' => null,
|
||||
'og_type' => null,
|
||||
'twitter_card' => null,
|
||||
'canonical_url' => null,
|
||||
], $rules);
|
||||
|
||||
expect($validator->passes())->toBeTrue();
|
||||
});
|
||||
|
||||
it('passes validation with valid data', function () {
|
||||
$form = createSeoForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'meta_description' => 'Valid meta description for SEO purposes',
|
||||
'meta_keywords' => 'keyword1, keyword2, keyword3',
|
||||
'meta_author' => 'Valid Author',
|
||||
'og_image' => 'https://example.com/image.jpg',
|
||||
'og_title' => 'Valid OG Title',
|
||||
'og_description' => 'Valid OG Description',
|
||||
'og_type' => 'article',
|
||||
'twitter_card' => 'summary_large_image',
|
||||
'canonical_url' => 'https://example.com/page',
|
||||
], $rules);
|
||||
|
||||
expect($validator->passes())->toBeTrue();
|
||||
});
|
||||
|
||||
it('has correct validation attributes', function () {
|
||||
$form = createSeoForm();
|
||||
$attributes = $form->validationAttributes();
|
||||
|
||||
expect($attributes)->toHaveKey('meta_description', 'meta deskripsi');
|
||||
expect($attributes)->toHaveKey('meta_keywords', 'meta kata kunci');
|
||||
expect($attributes)->toHaveKey('meta_author', 'meta penulis');
|
||||
expect($attributes)->toHaveKey('og_image', 'gambar Open Graph');
|
||||
expect($attributes)->toHaveKey('og_title', 'judul Open Graph');
|
||||
expect($attributes)->toHaveKey('og_description', 'deskripsi Open Graph');
|
||||
expect($attributes)->toHaveKey('og_type', 'tipe Open Graph');
|
||||
expect($attributes)->toHaveKey('twitter_card', 'kartu Twitter');
|
||||
expect($attributes)->toHaveKey('canonical_url', 'URL kanonis');
|
||||
});
|
||||
@ -1,117 +0,0 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Forms\Studio\Setting\SocialForm;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Livewire\Livewire;
|
||||
|
||||
function createSocialForm()
|
||||
{
|
||||
// Create a minimal Livewire component for testing
|
||||
$component = new class extends \Livewire\Component
|
||||
{
|
||||
public SocialForm $form;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->form = new SocialForm($this, 'form');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return '<div></div>';
|
||||
}
|
||||
};
|
||||
|
||||
$component->mount();
|
||||
|
||||
return $component->form;
|
||||
}
|
||||
|
||||
it('has correct validation rules', function () {
|
||||
$form = createSocialForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
expect($rules)->toHaveKey('facebook');
|
||||
expect($rules)->toHaveKey('instagram');
|
||||
expect($rules)->toHaveKey('youtube');
|
||||
expect($rules)->toHaveKey('tiktok');
|
||||
expect($rules)->toHaveKey('whatsapp');
|
||||
expect($rules)->toHaveKey('telegram');
|
||||
|
||||
foreach (['facebook', 'instagram', 'youtube', 'tiktok', 'whatsapp', 'telegram'] as $field) {
|
||||
expect($rules[$field])->toContain('nullable');
|
||||
expect($rules[$field])->toContain('url');
|
||||
expect($rules[$field])->toContain('max:255');
|
||||
}
|
||||
});
|
||||
|
||||
it('validates nullable fields accept null values', function () {
|
||||
$form = createSocialForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'facebook' => null,
|
||||
'instagram' => null,
|
||||
'youtube' => null,
|
||||
'tiktok' => null,
|
||||
'whatsapp' => null,
|
||||
'telegram' => null,
|
||||
], $rules);
|
||||
|
||||
expect($validator->passes())->toBeTrue();
|
||||
});
|
||||
|
||||
it('validates url format for social media fields', function () {
|
||||
$form = createSocialForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'facebook' => 'not-a-url',
|
||||
'instagram' => 'https://instagram.com/valid',
|
||||
'youtube' => 'invalid-url',
|
||||
'tiktok' => 'https://tiktok.com/@valid',
|
||||
'whatsapp' => 'not-a-url',
|
||||
'telegram' => 'https://t.me/valid',
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('facebook'))->toBeTrue();
|
||||
expect($validator->errors()->has('youtube'))->toBeTrue();
|
||||
expect($validator->errors()->has('whatsapp'))->toBeTrue();
|
||||
expect($validator->errors()->has('instagram'))->toBeFalse();
|
||||
expect($validator->errors()->has('tiktok'))->toBeFalse();
|
||||
expect($validator->errors()->has('telegram'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('validates url max length', function () {
|
||||
$form = createSocialForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'facebook' => 'https://facebook.com/'.str_repeat('a', 256),
|
||||
'instagram' => 'https://instagram.com/valid',
|
||||
'youtube' => 'https://youtube.com/valid',
|
||||
'tiktok' => 'https://tiktok.com/@valid',
|
||||
'whatsapp' => 'https://wa.me/valid',
|
||||
'telegram' => 'https://t.me/valid',
|
||||
], $rules);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
expect($validator->errors()->has('facebook'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('accepts valid urls', function () {
|
||||
$form = createSocialForm();
|
||||
$rules = $form->rules();
|
||||
|
||||
$validator = Validator::make([
|
||||
'facebook' => 'https://facebook.com/page',
|
||||
'instagram' => 'https://instagram.com/page',
|
||||
'youtube' => 'https://youtube.com/channel',
|
||||
'tiktok' => 'https://tiktok.com/@user',
|
||||
'whatsapp' => 'https://wa.me/123456789',
|
||||
'telegram' => 'https://t.me/channel',
|
||||
], $rules);
|
||||
|
||||
expect($validator->passes())->toBeTrue();
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user