major update
This commit is contained in:
@@ -20,7 +20,6 @@ This application is a Laravel application and its main Laravel ecosystems packag
|
||||
- livewire/flux (FLUXUI_FREE) - v2
|
||||
- livewire/livewire (LIVEWIRE) - v4
|
||||
- laravel/boost (BOOST) - v2
|
||||
- laravel/dusk (DUSK) - v8
|
||||
- laravel/mcp (MCP) - v0
|
||||
- laravel/pail (PAIL) - v1
|
||||
- laravel/pint (PINT) - v1
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
# WMS Fix Notes
|
||||
|
||||
This build patches only the current uploaded code.
|
||||
|
||||
## Fixed
|
||||
|
||||
- Duplicate two-factor migration made safe.
|
||||
- Primary two-factor migration adds columns one at a time for SQLite safety.
|
||||
- Module page slot is wrapped in a visible `.wms-content` main container.
|
||||
- Sidebar remains traditional: desktop/tablet icon rail, mobile drawer.
|
||||
- Sidebar widened for "Warehouse Management System".
|
||||
- Laravel cache folders are included so "Please provide a valid cache path" does not recur after unzip.
|
||||
|
||||
## After unzip
|
||||
|
||||
```bash
|
||||
composer install
|
||||
npm install
|
||||
php artisan optimize:clear
|
||||
php artisan test
|
||||
npm run build
|
||||
```
|
||||
@@ -0,0 +1,35 @@
|
||||
# Platform config and module menu fix
|
||||
|
||||
Applied fixes:
|
||||
|
||||
- Added `Modules/Platform/config/platform.php`.
|
||||
- Hardened `Modules/Platform/Providers/PlatformServiceProvider.php` so config loading is safe.
|
||||
- Removed stale generated files from `bootstrap/cache`.
|
||||
- Updated `resources/views/layouts/app.blade.php` so it works for both:
|
||||
- component pages using `<x-layouts::app>`
|
||||
- legacy placeholder module pages using `@extends('layouts.app')` and `@section('content')`
|
||||
- Added placeholder module entries to the left navigation:
|
||||
- Inventory Management
|
||||
- Shipping
|
||||
- Carrier Management
|
||||
- Transportation
|
||||
- Replenishment
|
||||
- M3 Integration
|
||||
- Document Archive
|
||||
- Analytics
|
||||
- Reporting
|
||||
- Audit
|
||||
- Security
|
||||
- Configuration
|
||||
- Added compatibility CSS for the placeholder module pages that use Bootstrap-style classes.
|
||||
- Ensured cache/runtime directories exist.
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
composer install
|
||||
npm install
|
||||
php artisan optimize:clear
|
||||
php artisan test
|
||||
npm run build
|
||||
```
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Fortify\Features;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AuthenticationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_login_screen_can_be_rendered(): void
|
||||
{
|
||||
$response = $this->get(route('login'));
|
||||
|
||||
$response->assertOk();
|
||||
}
|
||||
|
||||
public function test_users_can_authenticate_using_the_login_screen(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->csrfPost(route('login.store'), [
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('dashboard', absolute: false));
|
||||
|
||||
$this->assertAuthenticated();
|
||||
}
|
||||
|
||||
public function test_users_can_not_authenticate_with_invalid_password(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->csrfPost(route('login.store'), [
|
||||
'email' => $user->email,
|
||||
'password' => 'wrong-password',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrorsIn('email');
|
||||
|
||||
$this->assertGuest();
|
||||
}
|
||||
|
||||
public function test_users_with_two_factor_enabled_are_redirected_to_two_factor_challenge(): void
|
||||
{
|
||||
$this->skipUnlessFortifyHas(Features::twoFactorAuthentication());
|
||||
|
||||
Features::twoFactorAuthentication([
|
||||
'confirm' => true,
|
||||
'confirmPassword' => true,
|
||||
]);
|
||||
|
||||
$user = User::factory()->withTwoFactor()->create();
|
||||
|
||||
$response = $this->csrfPost(route('login.store'), [
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('two-factor.login'));
|
||||
$this->assertGuest();
|
||||
}
|
||||
|
||||
public function test_users_can_logout(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->csrfPost(route('logout'));
|
||||
|
||||
$response->assertRedirect(route('home'));
|
||||
|
||||
$this->assertGuest();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Laravel\Fortify\Features;
|
||||
use Tests\TestCase;
|
||||
|
||||
class EmailVerificationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->skipUnlessFortifyHas(Features::emailVerification());
|
||||
}
|
||||
|
||||
public function test_email_verification_screen_can_be_rendered(): void
|
||||
{
|
||||
$user = User::factory()->unverified()->create();
|
||||
|
||||
$response = $this->actingAs($user)->get(route('verification.notice'));
|
||||
|
||||
$response->assertOk();
|
||||
}
|
||||
|
||||
public function test_email_can_be_verified(): void
|
||||
{
|
||||
$user = User::factory()->unverified()->create();
|
||||
|
||||
Event::fake();
|
||||
|
||||
$verificationUrl = URL::temporarySignedRoute(
|
||||
'verification.verify',
|
||||
now()->addMinutes(60),
|
||||
['id' => $user->id, 'hash' => sha1($user->email)],
|
||||
);
|
||||
|
||||
$response = $this->actingAs($user)->get($verificationUrl);
|
||||
|
||||
Event::assertDispatched(Verified::class);
|
||||
|
||||
$this->assertTrue($user->fresh()->hasVerifiedEmail());
|
||||
$response->assertRedirect(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
|
||||
public function test_email_is_not_verified_with_invalid_hash(): void
|
||||
{
|
||||
$user = User::factory()->unverified()->create();
|
||||
|
||||
$verificationUrl = URL::temporarySignedRoute(
|
||||
'verification.verify',
|
||||
now()->addMinutes(60),
|
||||
['id' => $user->id, 'hash' => sha1('wrong-email')],
|
||||
);
|
||||
|
||||
$this->actingAs($user)->get($verificationUrl);
|
||||
|
||||
$this->assertFalse($user->fresh()->hasVerifiedEmail());
|
||||
}
|
||||
|
||||
public function test_already_verified_user_visiting_verification_link_is_redirected_without_firing_event_again(): void
|
||||
{
|
||||
$user = User::factory()->create([
|
||||
'email_verified_at' => now(),
|
||||
]);
|
||||
|
||||
Event::fake();
|
||||
|
||||
$verificationUrl = URL::temporarySignedRoute(
|
||||
'verification.verify',
|
||||
now()->addMinutes(60),
|
||||
['id' => $user->id, 'hash' => sha1($user->email)],
|
||||
);
|
||||
|
||||
$this->actingAs($user)->get($verificationUrl)
|
||||
->assertRedirect(route('dashboard', absolute: false).'?verified=1');
|
||||
|
||||
$this->assertTrue($user->fresh()->hasVerifiedEmail());
|
||||
Event::assertNotDispatched(Verified::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PasswordConfirmationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_confirm_password_screen_can_be_rendered(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->get(route('password.confirm'));
|
||||
|
||||
$response->assertOk();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Notifications\ResetPassword;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Notification;
|
||||
use Laravel\Fortify\Features;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PasswordResetTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->skipUnlessFortifyHas(Features::resetPasswords());
|
||||
}
|
||||
|
||||
public function test_reset_password_link_screen_can_be_rendered(): void
|
||||
{
|
||||
$response = $this->get(route('password.request'));
|
||||
|
||||
$response->assertOk();
|
||||
}
|
||||
|
||||
public function test_reset_password_link_can_be_requested(): void
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->csrfPost(route('password.email'), ['email' => $user->email]);
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class);
|
||||
}
|
||||
|
||||
public function test_reset_password_screen_can_be_rendered(): void
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->csrfPost(route('password.email'), ['email' => $user->email]);
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class, function ($notification) {
|
||||
$response = $this->get(route('password.reset', $notification->token));
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public function test_password_can_be_reset_with_valid_token(): void
|
||||
{
|
||||
Notification::fake();
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->csrfPost(route('password.email'), ['email' => $user->email]);
|
||||
|
||||
Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
|
||||
$response = $this->csrfPost(route('password.update'), [
|
||||
'token' => $notification->token,
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'password',
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('login', absolute: false));
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Fortify\Features;
|
||||
use Tests\TestCase;
|
||||
|
||||
class RegistrationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->skipUnlessFortifyHas(Features::registration());
|
||||
}
|
||||
|
||||
public function test_registration_screen_can_be_rendered(): void
|
||||
{
|
||||
$response = $this->get(route('register'));
|
||||
|
||||
$response->assertOk();
|
||||
}
|
||||
|
||||
public function test_new_users_can_register(): void
|
||||
{
|
||||
$response = $this->csrfPost(route('register.store'), [
|
||||
'name' => 'John Doe',
|
||||
'email' => 'test@example.com',
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'password',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasNoErrors()
|
||||
->assertRedirect(route('dashboard', absolute: false));
|
||||
|
||||
$this->assertAuthenticated();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Auth;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Fortify\Features;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TwoFactorChallengeTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->skipUnlessFortifyHas(Features::twoFactorAuthentication());
|
||||
}
|
||||
|
||||
public function test_two_factor_challenge_redirects_to_login_when_not_authenticated(): void
|
||||
{
|
||||
$response = $this->get(route('two-factor.login'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
}
|
||||
|
||||
public function test_two_factor_challenge_can_be_rendered(): void
|
||||
{
|
||||
Features::twoFactorAuthentication([
|
||||
'confirm' => true,
|
||||
'confirmPassword' => true,
|
||||
]);
|
||||
|
||||
$user = User::factory()->withTwoFactor()->create();
|
||||
|
||||
$this->csrfPost(route('login.store'), [
|
||||
'email' => $user->email,
|
||||
'password' => 'password',
|
||||
])->assertRedirect(route('two-factor.login'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class DashboardTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_guests_are_redirected_to_the_login_page(): void
|
||||
{
|
||||
$response = $this->get(route('dashboard'));
|
||||
$response->assertRedirect(route('login'));
|
||||
}
|
||||
|
||||
public function test_authenticated_users_can_visit_the_dashboard(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = $this->get(route('dashboard'));
|
||||
$response->assertOk();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_returns_a_successful_response(): void
|
||||
{
|
||||
$response = $this->get(route('home'));
|
||||
|
||||
$response->assertOk();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Settings;
|
||||
|
||||
use App\Livewire\Settings\Profile;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ProfileUpdateTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_profile_page_is_displayed(): void
|
||||
{
|
||||
$this->actingAs($user = User::factory()->create());
|
||||
|
||||
$this->get('/settings/profile')->assertOk();
|
||||
}
|
||||
|
||||
public function test_profile_information_can_be_updated(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = Livewire::test(Profile::class)
|
||||
->set('name', 'Test User')
|
||||
->set('email', 'test@example.com')
|
||||
->call('updateProfileInformation');
|
||||
|
||||
$response->assertHasNoErrors();
|
||||
|
||||
$user->refresh();
|
||||
|
||||
$this->assertEquals('Test User', $user->name);
|
||||
$this->assertEquals('test@example.com', $user->email);
|
||||
$this->assertNull($user->email_verified_at);
|
||||
}
|
||||
|
||||
public function test_email_verification_status_is_unchanged_when_email_address_is_unchanged(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = Livewire::test(Profile::class)
|
||||
->set('name', 'Test User')
|
||||
->set('email', $user->email)
|
||||
->call('updateProfileInformation');
|
||||
|
||||
$response->assertHasNoErrors();
|
||||
|
||||
$this->assertNotNull($user->refresh()->email_verified_at);
|
||||
}
|
||||
|
||||
public function test_user_can_delete_their_account(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = Livewire::test('settings.delete-user-form')
|
||||
->set('password', 'password')
|
||||
->call('deleteUser');
|
||||
|
||||
$response
|
||||
->assertHasNoErrors()
|
||||
->assertRedirect('/');
|
||||
|
||||
$this->assertNull($user->fresh());
|
||||
$this->assertFalse(auth()->check());
|
||||
}
|
||||
|
||||
public function test_correct_password_must_be_provided_to_delete_account(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = Livewire::test('settings.delete-user-form')
|
||||
->set('password', 'wrong-password')
|
||||
->call('deleteUser');
|
||||
|
||||
$response->assertHasErrors(['password']);
|
||||
|
||||
$this->assertNotNull($user->fresh());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Settings;
|
||||
|
||||
use App\Livewire\Settings\Security;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Laravel\Fortify\Features;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SecurityTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->skipUnlessFortifyHas(Features::twoFactorAuthentication());
|
||||
|
||||
Features::twoFactorAuthentication([
|
||||
'confirm' => true,
|
||||
'confirmPassword' => true,
|
||||
]);
|
||||
Features::passkeys([
|
||||
'confirmPassword' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_security_settings_page_can_be_rendered(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->withSession(['auth.password_confirmed_at' => time()])
|
||||
->get(route('security.edit'));
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$response->assertSee('Passkeys');
|
||||
$response->assertSee('No passkeys yet');
|
||||
$response->assertSee('Two-factor authentication');
|
||||
$response->assertSee('Enable 2FA');
|
||||
}
|
||||
|
||||
public function test_security_settings_page_requires_password_confirmation_when_enabled(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)
|
||||
->get(route('security.edit'));
|
||||
|
||||
$response->assertRedirect(route('password.confirm'));
|
||||
}
|
||||
|
||||
public function test_security_settings_page_renders_without_two_factor_when_feature_is_disabled(): void
|
||||
{
|
||||
config(['fortify.features' => []]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession(['auth.password_confirmed_at' => time()])
|
||||
->get(route('security.edit'))
|
||||
->assertOk()
|
||||
->assertSee('Update password')
|
||||
->assertDontSee('Manage your passkeys for passwordless sign-in')
|
||||
->assertDontSee('Add a passkey to sign in without a password')
|
||||
->assertDontSee('Two-factor authentication');
|
||||
}
|
||||
|
||||
public function test_two_factor_authentication_disabled_when_confirmation_abandoned_between_requests(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$user->forceFill([
|
||||
'two_factor_secret' => encrypt('test-secret'),
|
||||
'two_factor_recovery_codes' => encrypt(json_encode(['code1', 'code2'])),
|
||||
'two_factor_confirmed_at' => null,
|
||||
])->save();
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$component = Livewire::test(Security::class);
|
||||
|
||||
$component->assertSet('twoFactorEnabled', false);
|
||||
|
||||
$this->assertDatabaseHas('users', [
|
||||
'id' => $user->id,
|
||||
'two_factor_secret' => null,
|
||||
'two_factor_recovery_codes' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_password_can_be_updated(): void
|
||||
{
|
||||
$user = User::factory()->create([
|
||||
'password' => Hash::make('password'),
|
||||
]);
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = Livewire::test(Security::class)
|
||||
->set('current_password', 'password')
|
||||
->set('password', 'new-password')
|
||||
->set('password_confirmation', 'new-password')
|
||||
->call('updatePassword');
|
||||
|
||||
$response->assertHasNoErrors();
|
||||
|
||||
$this->assertTrue(Hash::check('new-password', $user->refresh()->password));
|
||||
}
|
||||
|
||||
public function test_correct_password_must_be_provided_to_update_password(): void
|
||||
{
|
||||
$user = User::factory()->create([
|
||||
'password' => Hash::make('password'),
|
||||
]);
|
||||
|
||||
$this->actingAs($user);
|
||||
|
||||
$response = Livewire::test(Security::class)
|
||||
->set('current_password', 'wrong-password')
|
||||
->set('password', 'new-password')
|
||||
->set('password_confirmation', 'new-password')
|
||||
->call('updatePassword');
|
||||
|
||||
$response->assertHasErrors(['current_password']);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
use Laravel\Fortify\Features;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
config([
|
||||
'app.debug' => false,
|
||||
'session.driver' => 'array',
|
||||
'cache.default' => 'array',
|
||||
'mail.default' => 'array',
|
||||
]);
|
||||
|
||||
$this->withoutMiddleware(ValidateCsrfToken::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST with a real CSRF token for Fortify/web form tests.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
protected function csrfPost(string $uri, array $data = []): \Illuminate\Testing\TestResponse
|
||||
{
|
||||
$token = 'test-csrf-token';
|
||||
|
||||
return $this->withSession(['_token' => $token])
|
||||
->post($uri, ['_token' => $token, ...$data]);
|
||||
}
|
||||
|
||||
protected function skipUnlessFortifyHas(string $feature, ?string $message = null): void
|
||||
{
|
||||
if (! Features::enabled($feature)) {
|
||||
$this->markTestSkipped($message ?? "Fortify feature [{$feature}] is not enabled.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_that_true_is_true(): void
|
||||
{
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,6 @@
|
||||
"require-dev": {
|
||||
"fakerphp/faker": "^1.24",
|
||||
"laravel/boost": "^2.4",
|
||||
"laravel/dusk": "^8.6",
|
||||
"laravel/pail": "^1.2.5",
|
||||
"laravel/pao": "^1.0.6",
|
||||
"laravel/pint": "^1.29",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<div {{ $getExtraAttributeBag() }}>
|
||||
{{ $getState() }}
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div>
|
||||
{{-- Use $getChildSchema() to render the child schema() of this component. --}}
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
<x-dynamic-component
|
||||
:component="$getEntryWrapperView()"
|
||||
:entry="$entry"
|
||||
>
|
||||
<div {{ $getExtraAttributeBag() }}>
|
||||
{{ $getState() }}
|
||||
</div>
|
||||
</x-dynamic-component>
|
||||
@@ -0,0 +1,11 @@
|
||||
<x-dynamic-component
|
||||
:component="$getFieldWrapperView()"
|
||||
:field="$field"
|
||||
>
|
||||
<div
|
||||
x-data="{ state: $wire.$entangle(@js($getStatePath())) }"
|
||||
{{ $getExtraAttributeBag() }}
|
||||
>
|
||||
{{-- Interact with the `state` property in Alpine.js --}}
|
||||
</div>
|
||||
</x-dynamic-component>
|
||||
@@ -0,0 +1,11 @@
|
||||
<div>
|
||||
<form wire:submit="{{ submitAction }}">
|
||||
{{ $this->form }}
|
||||
|
||||
<button type="submit">
|
||||
Submit
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<x-filament-actions::modals />
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
<div>
|
||||
{{ $this->content }}
|
||||
|
||||
<x-filament-actions::modals />
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div>
|
||||
{{ $this->table }}
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<x-filament-panels::page>
|
||||
{{-- Page content --}}
|
||||
</x-filament-panels::page>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div>
|
||||
{{-- Custom block preview --}}
|
||||
</div>
|
||||
@@ -0,0 +1,3 @@
|
||||
<div>
|
||||
{{-- Custom block --}}
|
||||
</div>
|
||||
@@ -0,0 +1,4 @@
|
||||
@import '../../../../vendor/filament/filament/resources/css/theme.css';
|
||||
|
||||
@source '../../../../app/Filament/{{ classDirectory }}**/*';
|
||||
@source '../../../../resources/views/filament/{{ viewDirectory }}**/*';
|
||||
@@ -0,0 +1,5 @@
|
||||
<x-filament-widgets::widget>
|
||||
<x-filament::section>
|
||||
{{-- Widget content --}}
|
||||
</x-filament::section>
|
||||
</x-filament-widgets::widget>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace {{ namespace }};
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\AppMeta;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\AppResource;
|
||||
|
||||
#[Description('A description of what this app resource does.')]
|
||||
#[AppMeta]
|
||||
class {{ class }} extends AppResource
|
||||
{
|
||||
/**
|
||||
* Handle the app resource request.
|
||||
*/
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
return Response::view('{{ view }}', [
|
||||
'title' => $this->title(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<x-mcp::app :title="$title">
|
||||
<x-slot:head>
|
||||
<script type="module">
|
||||
createMcpApp(async (app) => {
|
||||
document.getElementById('run-btn').addEventListener('click', async () => {
|
||||
const result = await app.callServerTool('tool-name', {});
|
||||
document.getElementById('output').textContent = result.content[0]?.text ?? '';
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</x-slot:head>
|
||||
|
||||
<div id="app">
|
||||
<button id="run-btn">Run</button>
|
||||
<p id="output"></p>
|
||||
</div>
|
||||
</x-mcp::app>
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace {{ namespace }};
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Prompt;
|
||||
use Laravel\Mcp\Server\Prompts\Argument;
|
||||
|
||||
#[Description('A description of what this prompt does.')]
|
||||
class {{ class }} extends Prompt
|
||||
{
|
||||
/**
|
||||
* Handle the prompt request.
|
||||
*/
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
//
|
||||
|
||||
return Response::text('The content generated by the prompt.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the prompt's arguments.
|
||||
*
|
||||
* @return array<int, Argument>
|
||||
*/
|
||||
public function arguments(): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace {{ namespace }};
|
||||
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Resource;
|
||||
|
||||
#[Description('A description of what this resource contains.')]
|
||||
class {{ class }} extends Resource
|
||||
{
|
||||
/**
|
||||
* Handle the resource request.
|
||||
*/
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
//
|
||||
|
||||
return Response::text('The resource content.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace {{ namespace }};
|
||||
|
||||
use Laravel\Mcp\Server;
|
||||
use Laravel\Mcp\Server\Attributes\Instructions;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Version;
|
||||
|
||||
#[Name('{{ serverDisplayName }}')]
|
||||
#[Version('0.0.1')]
|
||||
#[Instructions('Instructions describing how to use the server and its features.')]
|
||||
class {{ class }} extends Server
|
||||
{
|
||||
protected array $tools = [
|
||||
//
|
||||
];
|
||||
|
||||
protected array $resources = [
|
||||
//
|
||||
];
|
||||
|
||||
protected array $prompts = [
|
||||
//
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace {{ namespace }};
|
||||
|
||||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Description('A description of what this tool does.')]
|
||||
class {{ class }} extends Tool
|
||||
{
|
||||
/**
|
||||
* Handle the tool request.
|
||||
*/
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
//
|
||||
|
||||
return Response::text('The content generated by the tool.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tool's input schema.
|
||||
*
|
||||
* @return array<string, JsonSchema>
|
||||
*/
|
||||
public function schema(JsonSchema $schema): array
|
||||
{
|
||||
return [
|
||||
//
|
||||
];
|
||||
}
|
||||
}
|
||||
+51
-7
@@ -81,8 +81,8 @@ select:focus[data-flux-control] {
|
||||
top: 0;
|
||||
z-index: 40;
|
||||
display: flex;
|
||||
width: 21.5rem;
|
||||
min-width: 21.5rem;
|
||||
width: 24rem;
|
||||
min-width: 24rem;
|
||||
height: 100vh;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid rgb(228 228 231);
|
||||
@@ -143,7 +143,7 @@ select:focus[data-flux-control] {
|
||||
}
|
||||
|
||||
.wms-brand-title {
|
||||
max-width: 16rem;
|
||||
max-width: 18.5rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -164,6 +164,34 @@ select:focus[data-flux-control] {
|
||||
color: rgb(8 145 178);
|
||||
}
|
||||
|
||||
.wms-health-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgb(187 247 208);
|
||||
background: rgb(220 252 231);
|
||||
color: rgb(21 128 61);
|
||||
padding: 0.38rem 0.58rem;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 800;
|
||||
text-decoration: none;
|
||||
box-shadow: 0 10px 24px rgb(21 128 61 / 0.12);
|
||||
transition: transform 120ms ease, background 120ms ease, border-color 120ms ease;
|
||||
}
|
||||
|
||||
.wms-health-badge:hover {
|
||||
transform: translateY(-1px);
|
||||
background: rgb(187 247 208);
|
||||
border-color: rgb(134 239 172);
|
||||
}
|
||||
|
||||
.dark .wms-health-badge {
|
||||
border-color: rgb(20 83 45);
|
||||
background: rgb(20 83 45 / 0.55);
|
||||
color: rgb(187 247 208);
|
||||
}
|
||||
|
||||
.wms-sidebar-toggle,
|
||||
.wms-mobile-close,
|
||||
.wms-mobile-menu {
|
||||
@@ -277,6 +305,18 @@ select:focus[data-flux-control] {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.wms-platform-admin-link {
|
||||
min-height: 3rem;
|
||||
border: 1px solid rgb(14 116 144 / 0.28);
|
||||
background: linear-gradient(135deg, rgb(8 47 73 / 0.12), rgb(14 116 144 / 0.08));
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.dark .wms-platform-admin-link {
|
||||
border-color: rgb(103 232 249 / 0.22);
|
||||
background: linear-gradient(135deg, rgb(8 47 73 / 0.75), rgb(20 83 45 / 0.25));
|
||||
}
|
||||
|
||||
.wms-sidebar-footer {
|
||||
display: grid;
|
||||
gap: 0.25rem;
|
||||
@@ -372,6 +412,10 @@ select:focus[data-flux-control] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
html[data-wms-sidebar='collapsed'] .wms-health-badge {
|
||||
display: none;
|
||||
}
|
||||
|
||||
html[data-wms-sidebar='collapsed'] .wms-sidebar-header,
|
||||
html[data-wms-sidebar='collapsed'] .wms-sidebar-footer,
|
||||
html[data-wms-sidebar='collapsed'] .wms-sidebar-nav {
|
||||
@@ -411,8 +455,8 @@ select:focus[data-flux-control] {
|
||||
.wms-sidebar {
|
||||
position: fixed;
|
||||
inset: 0 auto 0 0;
|
||||
width: min(21.5rem, calc(100vw - 2.5rem));
|
||||
min-width: min(21.5rem, calc(100vw - 2.5rem));
|
||||
width: min(24rem, calc(100vw - 2.5rem));
|
||||
min-width: min(24rem, calc(100vw - 2.5rem));
|
||||
transform: translateX(-105%);
|
||||
box-shadow: 22px 0 60px rgb(0 0 0 / 0.24);
|
||||
}
|
||||
@@ -431,8 +475,8 @@ select:focus[data-flux-control] {
|
||||
}
|
||||
|
||||
html[data-wms-sidebar='collapsed'] .wms-sidebar {
|
||||
width: min(21.5rem, calc(100vw - 2.5rem));
|
||||
min-width: min(21.5rem, calc(100vw - 2.5rem));
|
||||
width: min(24rem, calc(100vw - 2.5rem));
|
||||
min-width: min(24rem, calc(100vw - 2.5rem));
|
||||
}
|
||||
|
||||
html[data-wms-sidebar='collapsed'] [data-wms-sidebar-label] {
|
||||
|
||||
@@ -21,6 +21,11 @@
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="/up" class="wms-health-badge" title="{{ __('Health Check') }}" aria-label="{{ __('Health Check') }}">
|
||||
<i class="fa-solid fa-heart-pulse" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('OK') }}</span>
|
||||
</a>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="wms-sidebar-toggle"
|
||||
@@ -46,14 +51,9 @@
|
||||
<div class="wms-sidebar-group">
|
||||
<div class="wms-sidebar-heading" data-wms-sidebar-label>{{ __('Operations') }}</div>
|
||||
|
||||
<a href="{{ route('dashboard') }}" wire:navigate class="wms-sidebar-link @if(request()->routeIs('dashboard')) is-active @endif" title="{{ __('Dashboard') }}">
|
||||
<i class="fa-solid fa-gauge-high wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Dashboard') }}</span>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('warehouses.index') }}" wire:navigate class="wms-sidebar-link @if(request()->routeIs('warehouses.*')) is-active @endif" title="{{ __('Warehouses') }}">
|
||||
<i class="fa-solid fa-warehouse wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Warehouses') }}</span>
|
||||
<a href="{{ route('dashboard') }}" wire:navigate class="wms-sidebar-link @if(request()->routeIs('dashboard')) is-active @endif" title="{{ __('Platform') }}">
|
||||
<i class="fa-solid fa-screwdriver-wrench wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Platform') }}</span>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('warehouse-rules.index') }}" wire:navigate class="wms-sidebar-link @if(request()->routeIs('warehouse-rules.*')) is-active @endif" title="{{ __('Validation Rules') }}">
|
||||
@@ -66,9 +66,9 @@
|
||||
<div class="wms-sidebar-group">
|
||||
<div class="wms-sidebar-heading" data-wms-sidebar-label>{{ __('Administration') }}</div>
|
||||
|
||||
<a href="{{ route('platform.dashboard') }}" wire:navigate class="wms-sidebar-link @if(request()->routeIs('platform.dashboard')) is-active @endif" title="{{ __('Admin Dashboard') }}">
|
||||
<i class="fa-solid fa-table-columns wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Admin Dashboard') }}</span>
|
||||
<a href="{{ route('warehouses.index') }}" wire:navigate class="wms-sidebar-link @if(request()->routeIs('warehouses.*')) is-active @endif" title="{{ __('Warehouses') }}">
|
||||
<i class="fa-solid fa-warehouse wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Warehouses') }}</span>
|
||||
</a>
|
||||
|
||||
<a href="{{ route('platform.companies.index') }}" wire:navigate class="wms-sidebar-link @if(request()->routeIs('platform.companies.*')) is-active @endif" title="{{ __('Companies') }}">
|
||||
@@ -90,12 +90,7 @@
|
||||
</nav>
|
||||
|
||||
<div class="wms-sidebar-footer">
|
||||
<a href="/up" class="wms-sidebar-link" title="{{ __('Health Check') }}">
|
||||
<i class="fa-solid fa-heart-pulse wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Health Check') }}</span>
|
||||
</a>
|
||||
|
||||
<a href="/admin" class="wms-sidebar-link" title="{{ __('Platform Admin') }}">
|
||||
<a href="/admin" class="wms-sidebar-link wms-platform-admin-link" title="{{ __('Platform Admin') }}">
|
||||
<i class="fa-solid fa-screwdriver-wrench wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label>{{ __('Platform Admin') }}</span>
|
||||
</a>
|
||||
|
||||
@@ -33,6 +33,11 @@
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<a href="/up" class="wms-health-badge" title="<?php echo e(__('Health Check')); ?>" aria-label="<?php echo e(__('Health Check')); ?>">
|
||||
<i class="fa-solid fa-heart-pulse" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label><?php echo e(__('OK')); ?></span>
|
||||
</a>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="wms-sidebar-toggle"
|
||||
@@ -58,14 +63,9 @@
|
||||
<div class="wms-sidebar-group">
|
||||
<div class="wms-sidebar-heading" data-wms-sidebar-label><?php echo e(__('Operations')); ?></div>
|
||||
|
||||
<a href="<?php echo e(route('dashboard')); ?>" wire:navigate class="wms-sidebar-link <?php if(request()->routeIs('dashboard')): ?> is-active <?php endif; ?>" title="<?php echo e(__('Dashboard')); ?>">
|
||||
<i class="fa-solid fa-gauge-high wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label><?php echo e(__('Dashboard')); ?></span>
|
||||
</a>
|
||||
|
||||
<a href="<?php echo e(route('warehouses.index')); ?>" wire:navigate class="wms-sidebar-link <?php if(request()->routeIs('warehouses.*')): ?> is-active <?php endif; ?>" title="<?php echo e(__('Warehouses')); ?>">
|
||||
<i class="fa-solid fa-warehouse wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label><?php echo e(__('Warehouses')); ?></span>
|
||||
<a href="<?php echo e(route('dashboard')); ?>" wire:navigate class="wms-sidebar-link <?php if(request()->routeIs('dashboard')): ?> is-active <?php endif; ?>" title="<?php echo e(__('Platform')); ?>">
|
||||
<i class="fa-solid fa-screwdriver-wrench wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label><?php echo e(__('Platform')); ?></span>
|
||||
</a>
|
||||
|
||||
<a href="<?php echo e(route('warehouse-rules.index')); ?>" wire:navigate class="wms-sidebar-link <?php if(request()->routeIs('warehouse-rules.*')): ?> is-active <?php endif; ?>" title="<?php echo e(__('Validation Rules')); ?>">
|
||||
@@ -78,9 +78,9 @@
|
||||
<div class="wms-sidebar-group">
|
||||
<div class="wms-sidebar-heading" data-wms-sidebar-label><?php echo e(__('Administration')); ?></div>
|
||||
|
||||
<a href="<?php echo e(route('platform.dashboard')); ?>" wire:navigate class="wms-sidebar-link <?php if(request()->routeIs('platform.dashboard')): ?> is-active <?php endif; ?>" title="<?php echo e(__('Admin Dashboard')); ?>">
|
||||
<i class="fa-solid fa-table-columns wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label><?php echo e(__('Admin Dashboard')); ?></span>
|
||||
<a href="<?php echo e(route('warehouses.index')); ?>" wire:navigate class="wms-sidebar-link <?php if(request()->routeIs('warehouses.*')): ?> is-active <?php endif; ?>" title="<?php echo e(__('Warehouses')); ?>">
|
||||
<i class="fa-solid fa-warehouse wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label><?php echo e(__('Warehouses')); ?></span>
|
||||
</a>
|
||||
|
||||
<a href="<?php echo e(route('platform.companies.index')); ?>" wire:navigate class="wms-sidebar-link <?php if(request()->routeIs('platform.companies.*')): ?> is-active <?php endif; ?>" title="<?php echo e(__('Companies')); ?>">
|
||||
@@ -102,12 +102,7 @@
|
||||
</nav>
|
||||
|
||||
<div class="wms-sidebar-footer">
|
||||
<a href="/up" class="wms-sidebar-link" title="<?php echo e(__('Health Check')); ?>">
|
||||
<i class="fa-solid fa-heart-pulse wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label><?php echo e(__('Health Check')); ?></span>
|
||||
</a>
|
||||
|
||||
<a href="/admin" class="wms-sidebar-link" title="<?php echo e(__('Platform Admin')); ?>">
|
||||
<a href="/admin" class="wms-sidebar-link wms-platform-admin-link" title="<?php echo e(__('Platform Admin')); ?>">
|
||||
<i class="fa-solid fa-screwdriver-wrench wms-sidebar-icon" aria-hidden="true"></i>
|
||||
<span data-wms-sidebar-label><?php echo e(__('Platform Admin')); ?></span>
|
||||
</a>
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Browser\Concerns;
|
||||
|
||||
use App\Models\User;
|
||||
use Modules\Platform\app\Models\Company;
|
||||
use Modules\Platform\app\Models\LicenseKey;
|
||||
use Modules\Platform\app\Models\Payment;
|
||||
use Modules\Warehouse\app\Models\Warehouse;
|
||||
use Modules\Warehouse\app\Models\WarehouseValidationRule;
|
||||
|
||||
trait CreatesWmsBrowserData
|
||||
{
|
||||
/**
|
||||
* Create a normal tenant user with an active licence.
|
||||
*/
|
||||
protected function createLicensedUser(array $attributes = []): User
|
||||
{
|
||||
return User::factory()->create($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an MPBA platform administrator attached to the system tenant.
|
||||
*/
|
||||
protected function createPlatformAdmin(array $attributes = []): User
|
||||
{
|
||||
return User::factory()->mpbaPlatformAdmin()->create($attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a warehouse for the user's company tenant.
|
||||
*/
|
||||
protected function createWarehouseFor(User $user, array $attributes = []): Warehouse
|
||||
{
|
||||
return Warehouse::query()->create([
|
||||
'company_id' => $user->company_id,
|
||||
'code' => $attributes['code'] ?? 'MAIN',
|
||||
'name' => $attributes['name'] ?? 'Main Warehouse',
|
||||
'status' => $attributes['status'] ?? 'active',
|
||||
'address' => $attributes['address'] ?? 'Unit 1, Test Estate',
|
||||
'timezone' => $attributes['timezone'] ?? 'Europe/London',
|
||||
'settings' => $attributes['settings'] ?? [],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a validation rule for a warehouse.
|
||||
*/
|
||||
protected function createWarehouseRule(Warehouse $warehouse, array $attributes = []): WarehouseValidationRule
|
||||
{
|
||||
return WarehouseValidationRule::query()->create([
|
||||
'warehouse_id' => $warehouse->id,
|
||||
'name' => $attributes['name'] ?? 'Location required',
|
||||
'rule_key' => $attributes['rule_key'] ?? 'stock.location.required',
|
||||
'severity' => $attributes['severity'] ?? 'error',
|
||||
'is_active' => $attributes['is_active'] ?? true,
|
||||
'configuration' => $attributes['configuration'] ?? ['required' => true],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a non-system tenant company for admin screens.
|
||||
*/
|
||||
protected function createTenantCompany(array $attributes = []): Company
|
||||
{
|
||||
$company = Company::query()->create([
|
||||
'name' => $attributes['name'] ?? 'Browser Tenant Ltd',
|
||||
'slug' => $attributes['slug'] ?? 'browser-tenant-ltd',
|
||||
'status' => $attributes['status'] ?? 'active',
|
||||
'timezone' => $attributes['timezone'] ?? 'Europe/London',
|
||||
'is_system_company' => $attributes['is_system_company'] ?? false,
|
||||
'billing_email' => $attributes['billing_email'] ?? 'billing@browser-tenant.example',
|
||||
'settings' => $attributes['settings'] ?? [],
|
||||
]);
|
||||
|
||||
LicenseKey::issueFor($company, 'Browser Test Licence', 'enterprise', 100);
|
||||
|
||||
return $company;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a payment row for platform payment tables.
|
||||
*/
|
||||
protected function createPaymentFor(Company $company, array $attributes = []): Payment
|
||||
{
|
||||
return Payment::query()->create([
|
||||
'company_id' => $company->id,
|
||||
'stripe_payment_intent_id' => $attributes['stripe_payment_intent_id'] ?? null,
|
||||
'stripe_invoice_id' => $attributes['stripe_invoice_id'] ?? null,
|
||||
'amount' => $attributes['amount'] ?? 129900,
|
||||
'currency' => $attributes['currency'] ?? 'gbp',
|
||||
'status' => $attributes['status'] ?? 'paid',
|
||||
'description' => $attributes['description'] ?? 'Annual platform licence',
|
||||
'paid_at' => $attributes['paid_at'] ?? now(),
|
||||
'metadata' => $attributes['metadata'] ?? ['source' => 'browser_test'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Browser\Concerns;
|
||||
|
||||
use Closure;
|
||||
use Laravel\Dusk\Browser;
|
||||
|
||||
trait TestsResponsiveBreakpoints
|
||||
{
|
||||
/**
|
||||
* @return array<string, array{width:int, height:int}>
|
||||
*/
|
||||
protected function responsiveBreakpoints(): array
|
||||
{
|
||||
return [
|
||||
'desktop' => ['width' => 1440, 'height' => 900],
|
||||
'laptop' => ['width' => 1280, 'height' => 800],
|
||||
'tablet_portrait' => ['width' => 768, 'height' => 1024],
|
||||
'tablet_landscape' => ['width' => 1024, 'height' => 768],
|
||||
'mobile' => ['width' => 390, 'height' => 844],
|
||||
'small_mobile' => ['width' => 360, 'height' => 740],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a browser assertion across every supported WMS viewport.
|
||||
*
|
||||
* @param Closure(Browser, string): void $callback
|
||||
*/
|
||||
protected function eachBreakpoint(Browser $browser, Closure $callback): void
|
||||
{
|
||||
foreach ($this->responsiveBreakpoints() as $name => $size) {
|
||||
$browser->resize($size['width'], $size['height']);
|
||||
$callback($browser, $name);
|
||||
$browser->screenshot("wms-responsive-{$name}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide whether the breakpoint should use the off-canvas mobile navigation.
|
||||
*/
|
||||
protected function isMobileNavigationBreakpoint(string $breakpoint): bool
|
||||
{
|
||||
return in_array($breakpoint, ['tablet_portrait', 'mobile', 'small_mobile'], true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Browser;
|
||||
|
||||
use Laravel\Dusk\Browser;
|
||||
use Tests\Browser\Concerns\CreatesWmsBrowserData;
|
||||
use Tests\Browser\Concerns\TestsResponsiveBreakpoints;
|
||||
use Tests\DuskTestCase;
|
||||
|
||||
class PlatformAdminUiTest extends DuskTestCase
|
||||
{
|
||||
use CreatesWmsBrowserData;
|
||||
use TestsResponsiveBreakpoints;
|
||||
|
||||
public function test_platform_admin_pages_render_at_all_breakpoints(): void
|
||||
{
|
||||
$admin = $this->createPlatformAdmin();
|
||||
$company = $this->createTenantCompany(['name' => 'Admin Browser Co', 'slug' => 'admin-browser-co']);
|
||||
$this->createWarehouseFor($admin, ['name' => 'System Warehouse', 'code' => 'SYS']);
|
||||
$this->createPaymentFor($company, ['description' => 'Browser payment']);
|
||||
|
||||
$pages = [
|
||||
['route' => 'platform.dashboard', 'heading' => 'Platform administration', 'expected' => ['Companies', 'Warehouses', 'Licences']],
|
||||
['route' => 'platform.companies.index', 'heading' => 'Companies', 'expected' => ['Admin Browser Co', 'Licences']],
|
||||
['route' => 'platform.licenses.index', 'heading' => 'Licence keys', 'expected' => ['Browser Test Licence', 'enterprise']],
|
||||
['route' => 'platform.payments.index', 'heading' => 'Payments and overrides', 'expected' => ['Browser payment', 'GBP']],
|
||||
];
|
||||
|
||||
$this->browse(function (Browser $browser) use ($admin, $pages): void {
|
||||
$browser->loginAs($admin);
|
||||
|
||||
foreach ($pages as $page) {
|
||||
$browser->visitRoute($page['route']);
|
||||
|
||||
$this->eachBreakpoint($browser, function (Browser $browser, string $breakpoint) use ($page): void {
|
||||
$browser->assertSee($page['heading']);
|
||||
|
||||
foreach ($page['expected'] as $text) {
|
||||
$browser->assertSee($text);
|
||||
}
|
||||
|
||||
if ($this->isMobileNavigationBreakpoint($breakpoint)) {
|
||||
$browser->assertVisible('[data-wms-mobile-open]');
|
||||
} else {
|
||||
$browser->assertVisible('[data-wms-sidebar]');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function test_platform_admin_can_create_company_licence_and_payment_from_ui(): void
|
||||
{
|
||||
$admin = $this->createPlatformAdmin();
|
||||
|
||||
$this->browse(function (Browser $browser) use ($admin): void {
|
||||
$browser
|
||||
->loginAs($admin)
|
||||
->visitRoute('platform.companies.index')
|
||||
->type('name', 'UI Created Company')
|
||||
->type('billing_email', 'accounts@ui-created.example')
|
||||
->select('status', 'active')
|
||||
->press('Create')
|
||||
->waitForText('UI Created Company')
|
||||
->assertSee('UI Created Company');
|
||||
|
||||
$companyId = (string) \Modules\Platform\app\Models\Company::query()
|
||||
->where('name', 'UI Created Company')
|
||||
->value('id');
|
||||
|
||||
$browser
|
||||
->visitRoute('platform.licenses.index')
|
||||
->select('company_id', $companyId)
|
||||
->type('label', 'UI Enterprise Licence')
|
||||
->select('plan', 'enterprise')
|
||||
->type('max_users', '250')
|
||||
->press('Issue')
|
||||
->waitForText('New key:')
|
||||
->assertSee('UI Enterprise Licence')
|
||||
->assertSee('enterprise');
|
||||
|
||||
$browser
|
||||
->visitRoute('platform.payments.index')
|
||||
->select('company_id', $companyId)
|
||||
->type('amount', '159900')
|
||||
->type('currency', 'gbp')
|
||||
->select('status', 'paid')
|
||||
->type('description', 'UI manual payment')
|
||||
->press('Add')
|
||||
->waitForText('UI manual payment')
|
||||
->assertSee('GBP 1,599.00')
|
||||
->assertSee('paid');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Browser;
|
||||
|
||||
use Laravel\Dusk\Browser;
|
||||
use Tests\Browser\Concerns\CreatesWmsBrowserData;
|
||||
use Tests\Browser\Concerns\TestsResponsiveBreakpoints;
|
||||
use Tests\DuskTestCase;
|
||||
|
||||
class WarehouseUiTest extends DuskTestCase
|
||||
{
|
||||
use CreatesWmsBrowserData;
|
||||
use TestsResponsiveBreakpoints;
|
||||
|
||||
public function test_dashboard_warehouse_and_rules_pages_render_at_all_breakpoints(): void
|
||||
{
|
||||
$user = $this->createLicensedUser(['name' => 'Tenant User']);
|
||||
$warehouse = $this->createWarehouseFor($user, ['name' => 'North Warehouse', 'code' => 'NORTH']);
|
||||
$this->createWarehouseRule($warehouse, ['name' => 'Batch number required']);
|
||||
|
||||
$pages = [
|
||||
['route' => 'dashboard', 'heading' => $user->company->name, 'expected' => ['Warehouses', 'Validation rules']],
|
||||
['route' => 'warehouses.index', 'heading' => 'Warehouses', 'expected' => ['North Warehouse', 'NORTH']],
|
||||
['route' => 'warehouse-rules.index', 'heading' => 'Warehouse validation rules', 'expected' => ['Batch number required', 'stock.location.required']],
|
||||
];
|
||||
|
||||
$this->browse(function (Browser $browser) use ($user, $pages): void {
|
||||
$browser->loginAs($user);
|
||||
|
||||
foreach ($pages as $page) {
|
||||
$browser->visitRoute($page['route']);
|
||||
|
||||
$this->eachBreakpoint($browser, function (Browser $browser, string $breakpoint) use ($page): void {
|
||||
$browser->assertSee($page['heading']);
|
||||
|
||||
foreach ($page['expected'] as $text) {
|
||||
$browser->assertSee($text);
|
||||
}
|
||||
|
||||
if ($this->isMobileNavigationBreakpoint($breakpoint)) {
|
||||
$browser->assertVisible('[data-wms-mobile-open]');
|
||||
} else {
|
||||
$browser->assertVisible('[data-wms-sidebar]');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function test_user_can_create_a_warehouse_from_the_ui(): void
|
||||
{
|
||||
$user = $this->createLicensedUser();
|
||||
|
||||
$this->browse(function (Browser $browser) use ($user): void {
|
||||
$browser
|
||||
->loginAs($user)
|
||||
->visitRoute('warehouses.index')
|
||||
->type('code', 'PICK')
|
||||
->type('name', 'Picking Warehouse')
|
||||
->select('status', 'active')
|
||||
->press('Create')
|
||||
->waitForText('Picking Warehouse')
|
||||
->assertSee('PICK')
|
||||
->assertSee('Picking Warehouse');
|
||||
});
|
||||
}
|
||||
|
||||
public function test_user_can_create_a_validation_rule_from_the_ui(): void
|
||||
{
|
||||
$user = $this->createLicensedUser();
|
||||
$warehouse = $this->createWarehouseFor($user, ['name' => 'Rules Warehouse', 'code' => 'RULES']);
|
||||
|
||||
$this->browse(function (Browser $browser) use ($user, $warehouse): void {
|
||||
$browser
|
||||
->loginAs($user)
|
||||
->visitRoute('warehouse-rules.index')
|
||||
->select('warehouse_id', (string) $warehouse->id)
|
||||
->type('name', 'Pallet scan required')
|
||||
->type('rule_key', 'pallet.scan.required')
|
||||
->select('severity', 'warning')
|
||||
->press('Add')
|
||||
->waitForText('Pallet scan required')
|
||||
->assertSee('pallet.scan.required')
|
||||
->assertSee('warning');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Browser;
|
||||
|
||||
use Laravel\Dusk\Browser;
|
||||
use Tests\Browser\Concerns\CreatesWmsBrowserData;
|
||||
use Tests\Browser\Concerns\TestsResponsiveBreakpoints;
|
||||
use Tests\DuskTestCase;
|
||||
|
||||
class WmsResponsiveShellTest extends DuskTestCase
|
||||
{
|
||||
use CreatesWmsBrowserData;
|
||||
use TestsResponsiveBreakpoints;
|
||||
|
||||
public function test_shell_renders_correct_desktop_tablet_and_mobile_navigation(): void
|
||||
{
|
||||
$user = $this->createPlatformAdmin([
|
||||
'name' => 'Mark Gregory',
|
||||
'email' => 'mark.gregory@example.test',
|
||||
]);
|
||||
|
||||
$this->browse(function (Browser $browser) use ($user): void {
|
||||
$browser->loginAs($user)->visitRoute('dashboard');
|
||||
|
||||
$this->eachBreakpoint($browser, function (Browser $browser, string $breakpoint): void {
|
||||
$browser
|
||||
->assertPresent('[data-wms-shell]')
|
||||
->assertPresent('[data-wms-sidebar]')
|
||||
->assertSee('Warehouse Management System')
|
||||
->assertSee('MPBA')
|
||||
->assertSee('Warehouses')
|
||||
->assertSee('Validation Rules')
|
||||
->assertSee('Platform Admin')
|
||||
->assertSee('Mark Gregory');
|
||||
|
||||
if ($this->isMobileNavigationBreakpoint($breakpoint)) {
|
||||
$browser
|
||||
->assertVisible('[data-wms-mobile-open]')
|
||||
->assertScript("document.documentElement.dataset.wmsMobileSidebar || 'closed'", 'closed')
|
||||
->click('[data-wms-mobile-open]')
|
||||
->waitUntil("document.documentElement.dataset.wmsMobileSidebar === 'open'")
|
||||
->assertScript("document.documentElement.dataset.wmsMobileSidebar", 'open')
|
||||
->click('[data-wms-mobile-close]')
|
||||
->waitUntil("document.documentElement.dataset.wmsMobileSidebar === 'closed'")
|
||||
->assertScript("document.documentElement.dataset.wmsMobileSidebar", 'closed');
|
||||
} else {
|
||||
$browser->assertVisible('[data-wms-sidebar-toggle]');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public function test_desktop_sidebar_collapses_expands_and_persists_state(): void
|
||||
{
|
||||
$user = $this->createPlatformAdmin();
|
||||
|
||||
$this->browse(function (Browser $browser) use ($user): void {
|
||||
$browser
|
||||
->resize(1440, 900)
|
||||
->loginAs($user)
|
||||
->visitRoute('dashboard')
|
||||
->script("localStorage.removeItem('wms-sidebar-state'); document.documentElement.dataset.wmsSidebar = 'expanded';");
|
||||
|
||||
$browser
|
||||
->refresh()
|
||||
->waitFor('[data-wms-sidebar-toggle]')
|
||||
->assertScript("document.documentElement.dataset.wmsSidebar", 'expanded')
|
||||
->click('[data-wms-sidebar-toggle]')
|
||||
->waitUntil("document.documentElement.dataset.wmsSidebar === 'collapsed'")
|
||||
->assertScript("localStorage.getItem('wms-sidebar-state')", 'collapsed')
|
||||
->assertAttribute('[data-wms-sidebar-toggle]', 'aria-expanded', 'false')
|
||||
->refresh()
|
||||
->waitFor('[data-wms-sidebar-toggle]')
|
||||
->assertScript("document.documentElement.dataset.wmsSidebar", 'collapsed')
|
||||
->click('[data-wms-sidebar-toggle]')
|
||||
->waitUntil("document.documentElement.dataset.wmsSidebar === 'expanded'")
|
||||
->assertScript("localStorage.getItem('wms-sidebar-state')", 'expanded')
|
||||
->assertAttribute('[data-wms-sidebar-toggle]', 'aria-expanded', 'true');
|
||||
});
|
||||
}
|
||||
|
||||
public function test_mobile_overlay_and_navigation_link_close_the_menu(): void
|
||||
{
|
||||
$user = $this->createPlatformAdmin();
|
||||
|
||||
$this->browse(function (Browser $browser) use ($user): void {
|
||||
$browser
|
||||
->resize(390, 844)
|
||||
->loginAs($user)
|
||||
->visitRoute('dashboard')
|
||||
->waitFor('[data-wms-mobile-open]')
|
||||
->click('[data-wms-mobile-open]')
|
||||
->waitUntil("document.documentElement.dataset.wmsMobileSidebar === 'open'")
|
||||
->click('[data-wms-mobile-close]')
|
||||
->waitUntil("document.documentElement.dataset.wmsMobileSidebar === 'closed'")
|
||||
->click('[data-wms-mobile-open]')
|
||||
->waitUntil("document.documentElement.dataset.wmsMobileSidebar === 'open'")
|
||||
->clickLink('Warehouses')
|
||||
->waitForLocation('/warehouses')
|
||||
->waitUntil("document.documentElement.dataset.wmsMobileSidebar === 'closed'")
|
||||
->assertSee('Warehouses');
|
||||
});
|
||||
}
|
||||
}
|
||||
+10
-19
@@ -5,21 +5,16 @@ namespace Tests;
|
||||
use Facebook\WebDriver\Chrome\ChromeOptions;
|
||||
use Facebook\WebDriver\Remote\DesiredCapabilities;
|
||||
use Facebook\WebDriver\Remote\RemoteWebDriver;
|
||||
use Illuminate\Support\Collection;
|
||||
use Laravel\Dusk\TestCase as BaseTestCase;
|
||||
use PHPUnit\Framework\Attributes\BeforeClass;
|
||||
|
||||
abstract class DuskTestCase extends BaseTestCase
|
||||
{
|
||||
/**
|
||||
* Prepare for Dusk test execution.
|
||||
*/
|
||||
#[BeforeClass]
|
||||
public static function prepare(): void
|
||||
{
|
||||
if (! static::runningInSail()) {
|
||||
static::startChromeDriver(['--port=9515']);
|
||||
}
|
||||
static::startChromeDriver();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -27,22 +22,18 @@ abstract class DuskTestCase extends BaseTestCase
|
||||
*/
|
||||
protected function driver(): RemoteWebDriver
|
||||
{
|
||||
$options = (new ChromeOptions)->addArguments(collect([
|
||||
$this->shouldStartMaximized() ? '--start-maximized' : '--window-size=1920,1080',
|
||||
'--disable-search-engine-choice-screen',
|
||||
'--disable-smooth-scrolling',
|
||||
])->unless($this->hasHeadlessDisabled(), function (Collection $items) {
|
||||
return $items->merge([
|
||||
'--disable-gpu',
|
||||
'--headless=new',
|
||||
]);
|
||||
})->all());
|
||||
$options = (new ChromeOptions)->addArguments(array_filter([
|
||||
'--disable-gpu',
|
||||
'--headless=new',
|
||||
'--window-size=1440,900',
|
||||
'--no-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--ignore-certificate-errors',
|
||||
]));
|
||||
|
||||
return RemoteWebDriver::create(
|
||||
$_ENV['DUSK_DRIVER_URL'] ?? env('DUSK_DRIVER_URL') ?? 'http://localhost:9515',
|
||||
DesiredCapabilities::chrome()->setCapability(
|
||||
ChromeOptions::CAPABILITY, $options
|
||||
)
|
||||
DesiredCapabilities::chrome()->setCapability(ChromeOptions::CAPABILITY, $options)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Modules;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ModulePlaceholderPageTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/**
|
||||
* @return array<string, array{route:string, heading:string, expected:array<int,string>}>
|
||||
*/
|
||||
public static function modulePages(): array
|
||||
{
|
||||
return [
|
||||
'analytics' => ['route' => 'analytics.index', 'heading' => 'Analytics', 'expected' => ['KPI1 Bar', 'OTTC', 'Warehouse Performance']],
|
||||
'audit' => ['route' => 'audit.index', 'heading' => 'Audit', 'expected' => ['Audit Trail', 'User Activity', 'Stock Adjustments']],
|
||||
'carrier-management' => ['route' => 'carrier-management.index', 'heading' => 'Carrier Management', 'expected' => ['Carrier Master', 'Service Levels']],
|
||||
'configuration' => ['route' => 'configuration.index', 'heading' => 'Configuration', 'expected' => ['System Parameters', 'Reference Data']],
|
||||
'document-archive' => ['route' => 'document-archive.index', 'heading' => 'Document Archive', 'expected' => ['POD Archive', 'Document Lookup']],
|
||||
'inventory-management' => ['route' => 'inventory-management.index', 'heading' => 'Inventory Management', 'expected' => ['Stock Enquiry', 'Stock Adjustments']],
|
||||
'm3-integration' => ['route' => 'm3-integration.index', 'heading' => 'M3 Integration', 'expected' => ['Inbound Interfaces', 'Outbound Interfaces']],
|
||||
'replenishment' => ['route' => 'replenishment.index', 'heading' => 'Replenishment', 'expected' => ['Shortage Monitor', 'Replenishment Tasks']],
|
||||
'reporting' => ['route' => 'reporting.index', 'heading' => 'Reporting', 'expected' => ['Operational Reports', 'Export Packs']],
|
||||
'security' => ['route' => 'security.index', 'heading' => 'Security', 'expected' => ['Users', 'Roles', 'Permissions']],
|
||||
'sentinel-integration' => ['route' => 'sentinel-integration.index', 'heading' => 'Sentinel Integration', 'expected' => ['Sentinel Sync', 'Message Queue']],
|
||||
'shipping' => ['route' => 'shipping.index', 'heading' => 'Shipping', 'expected' => ['Open Shipments', 'Dispatch Exceptions']],
|
||||
'transportation' => ['route' => 'transportation.index', 'heading' => 'Transportation', 'expected' => ['Pending Truck Closure', 'Freight Terms']],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider modulePages
|
||||
*
|
||||
* @param array<int,string> $expected
|
||||
*/
|
||||
public function test_module_placeholder_page_renders(string $route, string $heading, array $expected): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->get(route($route));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee($heading);
|
||||
$response->assertSee('Replacement module');
|
||||
$response->assertSee('workbench');
|
||||
|
||||
foreach ($expected as $text) {
|
||||
$response->assertSee($text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider modulePages
|
||||
*
|
||||
* @param array<int,string> $expected
|
||||
*/
|
||||
public function test_module_placeholder_page_requires_authentication(string $route, string $heading, array $expected): void
|
||||
{
|
||||
$this->get(route($route))->assertRedirect(route('login'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Modules;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Modules\Platform\app\Models\Company;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PlatformAdminUiFlowTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_platform_admin_can_view_and_manage_platform_records(): void
|
||||
{
|
||||
$admin = User::factory()->mpbaPlatformAdmin()->create();
|
||||
$company = Company::query()->create([
|
||||
'name' => 'Feature Tenant',
|
||||
'slug' => 'feature-tenant',
|
||||
'status' => 'active',
|
||||
'timezone' => 'Europe/London',
|
||||
'is_system_company' => false,
|
||||
'billing_email' => 'billing@feature-tenant.example',
|
||||
]);
|
||||
|
||||
$this->actingAs($admin)->get(route('platform.dashboard'))
|
||||
->assertOk()
|
||||
->assertSee('Platform administration')
|
||||
->assertSee('Feature Tenant');
|
||||
|
||||
$this->actingAs($admin)->post(route('platform.companies.store'), [
|
||||
'name' => 'Created Feature Co',
|
||||
'billing_email' => 'accounts@created-feature.example',
|
||||
'status' => 'active',
|
||||
])->assertRedirect();
|
||||
|
||||
$this->assertDatabaseHas('companies', [
|
||||
'slug' => 'created-feature-co',
|
||||
'billing_email' => 'accounts@created-feature.example',
|
||||
]);
|
||||
|
||||
$this->actingAs($admin)->post(route('platform.licenses.store'), [
|
||||
'company_id' => $company->id,
|
||||
'label' => 'Feature Licence',
|
||||
'plan' => 'enterprise',
|
||||
'max_users' => 250,
|
||||
])->assertSessionHas('plain_license_key');
|
||||
|
||||
$this->assertDatabaseHas('license_keys', [
|
||||
'company_id' => $company->id,
|
||||
'label' => 'Feature Licence',
|
||||
'plan' => 'enterprise',
|
||||
'max_users' => 250,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->actingAs($admin)->post(route('platform.payments.store'), [
|
||||
'company_id' => $company->id,
|
||||
'amount' => 99900,
|
||||
'currency' => 'gbp',
|
||||
'status' => 'paid',
|
||||
'description' => 'Feature payment',
|
||||
])->assertRedirect();
|
||||
|
||||
$this->assertDatabaseHas('payments', [
|
||||
'company_id' => $company->id,
|
||||
'amount' => 99900,
|
||||
'currency' => 'gbp',
|
||||
'status' => 'paid',
|
||||
'description' => 'Feature payment',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_normal_user_cannot_access_platform_admin(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)->get(route('platform.dashboard'))->assertForbidden();
|
||||
$this->actingAs($user)->get(route('platform.companies.index'))->assertForbidden();
|
||||
$this->actingAs($user)->get(route('platform.licenses.index'))->assertForbidden();
|
||||
$this->actingAs($user)->get(route('platform.payments.index'))->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_guest_is_redirected_from_platform_admin(): void
|
||||
{
|
||||
$this->get(route('platform.dashboard'))->assertRedirect(route('login'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Modules;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Modules\Platform\app\Models\Company;
|
||||
use Modules\Warehouse\app\Models\Warehouse;
|
||||
use Tests\TestCase;
|
||||
|
||||
class WarehouseUiFlowTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_licensed_user_can_view_dashboard_warehouses_and_rules(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$warehouse = Warehouse::query()->create([
|
||||
'company_id' => $user->company_id,
|
||||
'code' => 'MAIN',
|
||||
'name' => 'Main Warehouse',
|
||||
'status' => 'active',
|
||||
'timezone' => 'Europe/London',
|
||||
'settings' => [],
|
||||
]);
|
||||
|
||||
$this->actingAs($user)->get(route('dashboard'))
|
||||
->assertOk()
|
||||
->assertSee($user->company->name)
|
||||
->assertSee('Main Warehouse');
|
||||
|
||||
$this->actingAs($user)->get(route('warehouses.index'))
|
||||
->assertOk()
|
||||
->assertSee('Warehouses')
|
||||
->assertSee('MAIN')
|
||||
->assertSee('Main Warehouse');
|
||||
|
||||
$this->actingAs($user)->post(route('warehouse-rules.store'), [
|
||||
'warehouse_id' => $warehouse->id,
|
||||
'name' => 'Location required',
|
||||
'rule_key' => 'stock.location.required',
|
||||
'severity' => 'error',
|
||||
])->assertRedirect();
|
||||
|
||||
$this->actingAs($user)->get(route('warehouse-rules.index'))
|
||||
->assertOk()
|
||||
->assertSee('Warehouse validation rules')
|
||||
->assertSee('Location required')
|
||||
->assertSee('stock.location.required');
|
||||
}
|
||||
|
||||
public function test_user_can_create_warehouse_for_own_tenant_only(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$this->actingAs($user)->post(route('warehouses.store'), [
|
||||
'code' => 'PICK',
|
||||
'name' => 'Picking Warehouse',
|
||||
'status' => 'active',
|
||||
'address' => 'Rack 1',
|
||||
])->assertRedirect();
|
||||
|
||||
$this->assertDatabaseHas('warehouses', [
|
||||
'company_id' => $user->company_id,
|
||||
'code' => 'PICK',
|
||||
'name' => 'Picking Warehouse',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_unlicensed_tenant_gets_payment_required(): void
|
||||
{
|
||||
$company = Company::query()->create([
|
||||
'name' => 'Unlicensed Co',
|
||||
'slug' => 'unlicensed-co',
|
||||
'status' => 'active',
|
||||
'timezone' => 'Europe/London',
|
||||
'is_system_company' => false,
|
||||
]);
|
||||
|
||||
$user = User::factory()->create(['company_id' => $company->id]);
|
||||
|
||||
$this->actingAs($user)->get(route('dashboard'))->assertStatus(402);
|
||||
$this->actingAs($user)->get(route('warehouses.index'))->assertStatus(402);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user