diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index c541b6c39..79157b074 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -16,7 +16,7 @@ jobs:
strategy:
fail-fast: true
matrix:
- stack: [blade, react, vue, api]
+ stack: [blade, livewire, react, vue, api]
laravel: [10]
args: ["", --pest]
include:
diff --git a/src/Console/InstallCommand.php b/src/Console/InstallCommand.php
index f28583833..d71116c8e 100644
--- a/src/Console/InstallCommand.php
+++ b/src/Console/InstallCommand.php
@@ -19,14 +19,14 @@
class InstallCommand extends Command implements PromptsForMissingInput
{
- use InstallsApiStack, InstallsBladeStack, InstallsInertiaStacks;
+ use InstallsApiStack, InstallsBladeStack, InstallsInertiaStacks, InstallsLivewireStack;
/**
* The name and signature of the console command.
*
* @var string
*/
- protected $signature = 'breeze:install {stack : The development stack that should be installed (blade,react,vue,api)}
+ protected $signature = 'breeze:install {stack : The development stack that should be installed (blade,livewire,react,vue,api)}
{--dark : Indicate that dark mode support should be installed}
{--pest : Indicate that Pest should be installed}
{--ssr : Indicates if Inertia SSR support should be installed}
@@ -55,9 +55,11 @@ public function handle()
return $this->installApiStack();
} elseif ($this->argument('stack') === 'blade') {
return $this->installBladeStack();
+ } elseif ($this->argument('stack') === 'livewire') {
+ return $this->installLivewireStack();
}
- $this->components->error('Invalid stack. Supported stacks are [blade], [react], [vue], and [api].');
+ $this->components->error('Invalid stack. Supported stacks are [blade], [livewire], [react], [vue], and [api].');
return 1;
}
@@ -71,7 +73,11 @@ protected function installTests()
{
(new Filesystem)->ensureDirectoryExists(base_path('tests/Feature'));
- $stubStack = $this->argument('stack') === 'api' ? 'api' : 'default';
+ $stubStack = match ($this->argument('stack')) {
+ 'api' => 'api',
+ 'livewire' => 'livewire',
+ default => 'default',
+ };
if ($this->option('pest') || $this->isUsingPest()) {
if ($this->hasComposerPackage('phpunit/phpunit')) {
@@ -308,6 +314,7 @@ protected function promptForMissingArgumentsUsing()
label: 'Which Breeze stack would you like to install?',
options: [
'blade' => 'Blade with Alpine',
+ 'livewire' => 'Livewire with Alpine',
'react' => 'React with Inertia',
'vue' => 'Vue with Inertia',
'api' => 'API only',
@@ -336,7 +343,7 @@ protected function afterPromptingForMissingArguments(InputInterface $input, Outp
'typescript' => 'TypeScript (experimental)',
]
))->each(fn ($option) => $input->setOption($option, true));
- } elseif ($stack === 'blade') {
+ } elseif (in_array($stack, ['blade', 'livewire'])) {
$input->setOption('dark', confirm(
label: 'Would you like dark mode support?',
default: false
diff --git a/src/Console/InstallsLivewireStack.php b/src/Console/InstallsLivewireStack.php
new file mode 100644
index 000000000..525b307e0
--- /dev/null
+++ b/src/Console/InstallsLivewireStack.php
@@ -0,0 +1,101 @@
+updateNodePackages(function ($packages) {
+ return [
+ '@tailwindcss/forms' => '^0.5.2',
+ 'autoprefixer' => '^10.4.2',
+ 'postcss' => '^8.4.6',
+ 'tailwindcss' => '^3.1.0',
+ ] + $packages;
+ });
+
+ // Install Livewire...
+ if (! $this->requireComposerPackages(['livewire/livewire:^3.0', 'livewire/volt:^1.0'])) {
+ return 1;
+ }
+
+ // Install Volt...
+ (new Process([$this->phpBinary(), 'artisan', 'volt:install'], base_path()))
+ ->setTimeout(null)
+ ->run();
+
+ // Controllers
+ (new Filesystem)->ensureDirectoryExists(app_path('Http/Controllers/Auth'));
+ (new Filesystem)->copy(
+ __DIR__.'/../../stubs/default/app/Http/Controllers/Auth/VerifyEmailController.php',
+ app_path('Http/Controllers/Auth/VerifyEmailController.php'),
+ );
+
+ // Views...
+ (new Filesystem)->ensureDirectoryExists(resource_path('views'));
+ (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/livewire/resources/views', resource_path('views'));
+
+ // Views Components...
+ (new Filesystem)->ensureDirectoryExists(resource_path('views/components'));
+ (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/default/resources/views/components', resource_path('views/components'));
+ (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/livewire/resources/views/components', resource_path('views/components'));
+
+ // Views Layouts...
+ (new Filesystem)->ensureDirectoryExists(resource_path('views/layouts'));
+ (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/livewire/resources/views/layouts', resource_path('views/layouts'));
+
+ // Components...
+ (new Filesystem)->ensureDirectoryExists(app_path('View/Components'));
+ (new Filesystem)->copyDirectory(__DIR__.'/../../stubs/default/app/View/Components', app_path('View/Components'));
+
+ // Dark mode...
+ if (! $this->option('dark')) {
+ $this->removeDarkClasses((new Finder)
+ ->in(resource_path('views'))
+ ->name('*.blade.php')
+ ->notName('welcome.blade.php')
+ );
+ }
+
+ // Tests...
+ if (! $this->installTests()) {
+ return 1;
+ }
+
+ // Routes...
+ copy(__DIR__.'/../../stubs/livewire/routes/web.php', base_path('routes/web.php'));
+ copy(__DIR__.'/../../stubs/livewire/routes/auth.php', base_path('routes/auth.php'));
+
+ // "Dashboard" Route...
+ $this->replaceInFile('/home', '/dashboard', app_path('Providers/RouteServiceProvider.php'));
+
+ // Tailwind / Vite...
+ copy(__DIR__.'/../../stubs/default/tailwind.config.js', base_path('tailwind.config.js'));
+ copy(__DIR__.'/../../stubs/default/postcss.config.js', base_path('postcss.config.js'));
+ copy(__DIR__.'/../../stubs/default/vite.config.js', base_path('vite.config.js'));
+ copy(__DIR__.'/../../stubs/default/resources/css/app.css', resource_path('css/app.css'));
+
+ $this->components->info('Installing and building Node dependencies.');
+
+ if (file_exists(base_path('pnpm-lock.yaml'))) {
+ $this->runCommands(['pnpm install', 'pnpm run build']);
+ } elseif (file_exists(base_path('yarn.lock'))) {
+ $this->runCommands(['yarn install', 'yarn run build']);
+ } else {
+ $this->runCommands(['npm install', 'npm run build']);
+ }
+
+ $this->components->info('Livewire scaffolding installed successfully.');
+ }
+}
diff --git a/stubs/livewire/pest-tests/Feature/Auth/AuthenticationTest.php b/stubs/livewire/pest-tests/Feature/Auth/AuthenticationTest.php
new file mode 100644
index 000000000..f4f043979
--- /dev/null
+++ b/stubs/livewire/pest-tests/Feature/Auth/AuthenticationTest.php
@@ -0,0 +1,73 @@
+get('/login');
+
+ $response
+ ->assertSeeVolt('pages.auth.login')
+ ->assertOk();
+});
+
+test('users can authenticate using the login screen', function () {
+ $user = User::factory()->create();
+
+ $component = Volt::test('pages.auth.login')
+ ->set('email', $user->email)
+ ->set('password', 'password');
+
+ $component->call('login');
+
+ $component
+ ->assertHasNoErrors()
+ ->assertRedirect(RouteServiceProvider::HOME);
+
+ $this->assertAuthenticated();
+});
+
+test('users can not authenticate with invalid password', function () {
+ $user = User::factory()->create();
+
+ $component = Volt::test('pages.auth.login')
+ ->set('email', $user->email)
+ ->set('password', 'wrong-password');
+
+ $component->call('login');
+
+ $component
+ ->assertHasErrors()
+ ->assertNoRedirect();
+
+ $this->assertGuest();
+});
+
+test('navigation menu can be rendered', function () {
+ $user = User::factory()->create();
+
+ $this->actingAs($user);
+
+ $response = $this->get('/dashboard');
+
+ $response
+ ->assertSeeVolt('layout.navigation')
+ ->assertOk();
+});
+
+test('users can logout', function () {
+ $user = User::factory()->create();
+
+ $this->actingAs($user);
+
+ $component = Volt::test('layout.navigation');
+
+ $component->call('logout');
+
+ $component
+ ->assertHasNoErrors()
+ ->assertRedirect('/');
+
+ $this->assertGuest();
+});
diff --git a/stubs/livewire/pest-tests/Feature/Auth/EmailVerificationTest.php b/stubs/livewire/pest-tests/Feature/Auth/EmailVerificationTest.php
new file mode 100644
index 000000000..0e6a6d1d7
--- /dev/null
+++ b/stubs/livewire/pest-tests/Feature/Auth/EmailVerificationTest.php
@@ -0,0 +1,53 @@
+create([
+ 'email_verified_at' => null,
+ ]);
+
+ $response = $this->actingAs($user)->get('/verify-email');
+
+ $response->assertStatus(200);
+});
+
+test('email can be verified', function () {
+ $user = User::factory()->create([
+ 'email_verified_at' => null,
+ ]);
+
+ 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);
+ expect($user->fresh()->hasVerifiedEmail())->toBeTrue();
+ $response->assertRedirect(RouteServiceProvider::HOME.'?verified=1');
+});
+
+test('email is not verified with invalid hash', function () {
+ $user = User::factory()->create([
+ 'email_verified_at' => null,
+ ]);
+
+ $verificationUrl = URL::temporarySignedRoute(
+ 'verification.verify',
+ now()->addMinutes(60),
+ ['id' => $user->id, 'hash' => sha1('wrong-email')]
+ );
+
+ $this->actingAs($user)->get($verificationUrl);
+
+ expect($user->fresh()->hasVerifiedEmail())->toBeFalse();
+});
diff --git a/stubs/livewire/pest-tests/Feature/Auth/PasswordConfirmationTest.php b/stubs/livewire/pest-tests/Feature/Auth/PasswordConfirmationTest.php
new file mode 100644
index 000000000..21c28c39b
--- /dev/null
+++ b/stubs/livewire/pest-tests/Feature/Auth/PasswordConfirmationTest.php
@@ -0,0 +1,46 @@
+create();
+
+ $response = $this->actingAs($user)->get('/confirm-password');
+
+ $response
+ ->assertSeeVolt('pages.auth.confirm-password')
+ ->assertStatus(200);
+});
+
+test('password can be confirmed', function () {
+ $user = User::factory()->create();
+
+ $this->actingAs($user);
+
+ $component = Volt::test('pages.auth.confirm-password')
+ ->set('password', 'password');
+
+ $component->call('confirmPassword');
+
+ $component
+ ->assertRedirect('/dashboard')
+ ->assertHasNoErrors();
+});
+
+test('password is not confirmed with invalid password', function () {
+ $user = User::factory()->create();
+
+ $this->actingAs($user);
+
+ $component = Volt::test('pages.auth.confirm-password')
+ ->set('password', 'wrong-password');
+
+ $component->call('confirmPassword');
+
+ $component
+ ->assertNoRedirect()
+ ->assertHasErrors('password');
+});
diff --git a/stubs/livewire/pest-tests/Feature/Auth/PasswordResetTest.php b/stubs/livewire/pest-tests/Feature/Auth/PasswordResetTest.php
new file mode 100644
index 000000000..c478bce9f
--- /dev/null
+++ b/stubs/livewire/pest-tests/Feature/Auth/PasswordResetTest.php
@@ -0,0 +1,73 @@
+get('/forgot-password');
+
+ $response
+ ->assertSeeVolt('pages.auth.forgot-password')
+ ->assertStatus(200);
+});
+
+test('reset password link can be requested', function () {
+ Notification::fake();
+
+ $user = User::factory()->create();
+
+ Volt::test('pages.auth.forgot-password')
+ ->set('email', $user->email)
+ ->call('sendPasswordResetLink');
+
+ Notification::assertSentTo($user, ResetPassword::class);
+});
+
+test('reset password screen can be rendered', function () {
+ Notification::fake();
+
+ $user = User::factory()->create();
+
+ Volt::test('pages.auth.forgot-password')
+ ->set('email', $user->email)
+ ->call('sendPasswordResetLink');
+
+ Notification::assertSentTo($user, ResetPassword::class, function ($notification) {
+ $response = $this->get('/reset-password/'.$notification->token);
+
+ $response
+ ->assertSeeVolt('pages.auth.reset-password')
+ ->assertStatus(200);
+
+ return true;
+ });
+});
+
+test('password can be reset with valid token', function () {
+ Notification::fake();
+
+ $user = User::factory()->create();
+
+ Volt::test('pages.auth.forgot-password')
+ ->set('email', $user->email)
+ ->call('sendPasswordResetLink');
+
+ Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
+ $component = Volt::test('pages.auth.reset-password', ['token' => $notification->token])
+ ->set('email', $user->email)
+ ->set('password', 'password')
+ ->set('password_confirmation', 'password');
+
+ $component->call('resetPassword');
+
+ $component
+ ->assertRedirect('/login')
+ ->assertHasNoErrors();
+
+ return true;
+ });
+});
diff --git a/stubs/livewire/pest-tests/Feature/Auth/PasswordUpdateTest.php b/stubs/livewire/pest-tests/Feature/Auth/PasswordUpdateTest.php
new file mode 100644
index 000000000..33b1d4b50
--- /dev/null
+++ b/stubs/livewire/pest-tests/Feature/Auth/PasswordUpdateTest.php
@@ -0,0 +1,41 @@
+create();
+
+ $this->actingAs($user);
+
+ $component = Volt::test('profile.update-password-form')
+ ->set('current_password', 'password')
+ ->set('password', 'new-password')
+ ->set('password_confirmation', 'new-password')
+ ->call('updatePassword');
+
+ $component
+ ->assertHasNoErrors()
+ ->assertNoRedirect();
+
+ $this->assertTrue(Hash::check('new-password', $user->refresh()->password));
+});
+
+test('correct password must be provided to update password', function () {
+ $user = User::factory()->create();
+
+ $this->actingAs($user);
+
+ $component = Volt::test('profile.update-password-form')
+ ->set('current_password', 'wrong-password')
+ ->set('password', 'new-password')
+ ->set('password_confirmation', 'new-password')
+ ->call('updatePassword');
+
+ $component
+ ->assertHasErrors(['current_password'])
+ ->assertNoRedirect();
+});
diff --git a/stubs/livewire/pest-tests/Feature/Auth/RegistrationTest.php b/stubs/livewire/pest-tests/Feature/Auth/RegistrationTest.php
new file mode 100644
index 000000000..6952ce84d
--- /dev/null
+++ b/stubs/livewire/pest-tests/Feature/Auth/RegistrationTest.php
@@ -0,0 +1,28 @@
+get('/register');
+
+ $response
+ ->assertSeeVolt('pages.auth.register')
+ ->assertOk();
+});
+
+test('new users can register', function () {
+ $component = Volt::test('pages.auth.register')
+ ->set('name', 'Test User')
+ ->set('email', 'test@example.com')
+ ->set('password', 'password')
+ ->set('password_confirmation', 'password');
+
+ $component->call('register');
+
+ $component->assertRedirect(RouteServiceProvider::HOME);
+
+ $this->assertAuthenticated();
+});
diff --git a/stubs/livewire/pest-tests/Feature/ExampleTest.php b/stubs/livewire/pest-tests/Feature/ExampleTest.php
new file mode 100644
index 000000000..8b5843f49
--- /dev/null
+++ b/stubs/livewire/pest-tests/Feature/ExampleTest.php
@@ -0,0 +1,7 @@
+get('/');
+
+ $response->assertStatus(200);
+});
diff --git a/stubs/livewire/pest-tests/Feature/ProfileTest.php b/stubs/livewire/pest-tests/Feature/ProfileTest.php
new file mode 100644
index 000000000..b84a5e066
--- /dev/null
+++ b/stubs/livewire/pest-tests/Feature/ProfileTest.php
@@ -0,0 +1,89 @@
+create();
+
+ $this->actingAs($user);
+
+ $response = $this->get('/profile');
+
+ $response
+ ->assertSeeVolt('profile.update-profile-information-form')
+ ->assertSeeVolt('profile.update-password-form')
+ ->assertSeeVolt('profile.delete-user-form')
+ ->assertOk();
+});
+
+test('profile information can be updated', function () {
+ $user = User::factory()->create();
+
+ $this->actingAs($user);
+
+ $component = Volt::test('profile.update-profile-information-form')
+ ->set('name', 'Test User')
+ ->set('email', 'test@example.com')
+ ->call('updateProfileInformation');
+
+ $component
+ ->assertHasNoErrors()
+ ->assertNoRedirect();
+
+ $user->refresh();
+
+ $this->assertSame('Test User', $user->name);
+ $this->assertSame('test@example.com', $user->email);
+ $this->assertNull($user->email_verified_at);
+});
+
+test('email verification status is unchanged when the email address is unchanged', function () {
+ $user = User::factory()->create();
+
+ $this->actingAs($user);
+
+ $component = Volt::test('profile.update-profile-information-form')
+ ->set('name', 'Test User')
+ ->set('email', $user->email)
+ ->call('updateProfileInformation');
+
+ $component
+ ->assertHasNoErrors()
+ ->assertNoRedirect();
+
+ $this->assertNotNull($user->refresh()->email_verified_at);
+});
+
+test('user can delete their account', function () {
+ $user = User::factory()->create();
+
+ $this->actingAs($user);
+
+ $component = Volt::test('profile.delete-user-form')
+ ->set('password', 'password')
+ ->call('deleteUser');
+
+ $component
+ ->assertHasNoErrors()
+ ->assertRedirect('/');
+
+ $this->assertGuest();
+ $this->assertNull($user->fresh());
+});
+
+test('correct password must be provided to delete account', function () {
+ $user = User::factory()->create();
+
+ $this->actingAs($user);
+
+ $component = Volt::test('profile.delete-user-form')
+ ->set('password', 'wrong-password')
+ ->call('deleteUser');
+
+ $component
+ ->assertHasErrors('password')
+ ->assertNoRedirect();
+
+ $this->assertNotNull($user->fresh());
+});
diff --git a/stubs/livewire/pest-tests/Pest.php b/stubs/livewire/pest-tests/Pest.php
new file mode 100644
index 000000000..e2eb38087
--- /dev/null
+++ b/stubs/livewire/pest-tests/Pest.php
@@ -0,0 +1,48 @@
+in('Feature');
+
+/*
+|--------------------------------------------------------------------------
+| Expectations
+|--------------------------------------------------------------------------
+|
+| When you're writing tests, you often need to check that values meet certain conditions. The
+| "expect()" function gives you access to a set of "expectations" methods that you can use
+| to assert different things. Of course, you may extend the Expectation API at any time.
+|
+*/
+
+expect()->extend('toBeOne', function () {
+ return $this->toBe(1);
+});
+
+/*
+|--------------------------------------------------------------------------
+| Functions
+|--------------------------------------------------------------------------
+|
+| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
+| project that you don't want to repeat in every file. Here you can also expose helpers as
+| global functions to help you to reduce the number of lines of code in your test files.
+|
+*/
+
+function something()
+{
+ // ..
+}
diff --git a/stubs/livewire/pest-tests/Unit/ExampleTest.php b/stubs/livewire/pest-tests/Unit/ExampleTest.php
new file mode 100644
index 000000000..44a4f337a
--- /dev/null
+++ b/stubs/livewire/pest-tests/Unit/ExampleTest.php
@@ -0,0 +1,5 @@
+toBeTrue();
+});
diff --git a/stubs/livewire/resources/views/components/action-message.blade.php b/stubs/livewire/resources/views/components/action-message.blade.php
new file mode 100644
index 000000000..46ac232ad
--- /dev/null
+++ b/stubs/livewire/resources/views/components/action-message.blade.php
@@ -0,0 +1,10 @@
+@props(['on'])
+
+
merge(['class' => 'text-sm text-gray-600 dark:text-gray-400']) }}>
+ {{ $slot->isEmpty() ? 'Saved.' : $slot }}
+
diff --git a/stubs/livewire/resources/views/dashboard.blade.php b/stubs/livewire/resources/views/dashboard.blade.php
new file mode 100644
index 000000000..4024c64a8
--- /dev/null
+++ b/stubs/livewire/resources/views/dashboard.blade.php
@@ -0,0 +1,17 @@
+
+
+
+ {{ __('Dashboard') }}
+
+
+
+
+
+
+
+ {{ __("You're logged in!") }}
+
+
+
+
+
diff --git a/stubs/livewire/resources/views/layouts/app.blade.php b/stubs/livewire/resources/views/layouts/app.blade.php
new file mode 100644
index 000000000..89c7cd546
--- /dev/null
+++ b/stubs/livewire/resources/views/layouts/app.blade.php
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+ {{ config('app.name', 'Laravel') }}
+
+
+
+
+
+
+ @vite(['resources/css/app.css', 'resources/js/app.js'])
+
+
+
+
+
+
+ @if (isset($header))
+
+ @endif
+
+
+
+ {{ $slot }}
+
+
+
+
diff --git a/stubs/livewire/resources/views/layouts/guest.blade.php b/stubs/livewire/resources/views/layouts/guest.blade.php
new file mode 100644
index 000000000..eaa606538
--- /dev/null
+++ b/stubs/livewire/resources/views/layouts/guest.blade.php
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+ {{ config('app.name', 'Laravel') }}
+
+
+
+
+
+
+ @vite(['resources/css/app.css', 'resources/js/app.js'])
+
+
+
+
+
diff --git a/stubs/livewire/resources/views/livewire/layout/navigation.blade.php b/stubs/livewire/resources/views/livewire/layout/navigation.blade.php
new file mode 100644
index 000000000..8efb8b04e
--- /dev/null
+++ b/stubs/livewire/resources/views/livewire/layout/navigation.blade.php
@@ -0,0 +1,109 @@
+guard('web')->logout();
+
+ session()->invalidate();
+ session()->regenerateToken();
+
+ $this->redirect('/', navigate: true);
+ }
+}; ?>
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ __('Dashboard') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ __('Profile') }}
+
+
+
+
+
+ {{ __('Log Out') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ __('Dashboard') }}
+
+
+
+
+
+
+
+
{{ auth()->user()->email }}
+
+
+
+
+ {{ __('Profile') }}
+
+
+
+
+
+ {{ __('Log Out') }}
+
+
+
+
+
+
diff --git a/stubs/livewire/resources/views/livewire/pages/auth/confirm-password.blade.php b/stubs/livewire/resources/views/livewire/pages/auth/confirm-password.blade.php
new file mode 100644
index 000000000..9d9325004
--- /dev/null
+++ b/stubs/livewire/resources/views/livewire/pages/auth/confirm-password.blade.php
@@ -0,0 +1,62 @@
+validate();
+
+ if (! auth()->guard('web')->validate([
+ 'email' => auth()->user()->email,
+ 'password' => $this->password,
+ ])) {
+ throw ValidationException::withMessages([
+ 'password' => __('auth.password'),
+ ]);
+ }
+
+ session(['auth.password_confirmed_at' => time()]);
+
+ $this->redirect(
+ session('url.intended', RouteServiceProvider::HOME),
+ navigate: true
+ );
+ }
+}; ?>
+
+
+
+ {{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
+
+
+
+
diff --git a/stubs/livewire/resources/views/livewire/pages/auth/forgot-password.blade.php b/stubs/livewire/resources/views/livewire/pages/auth/forgot-password.blade.php
new file mode 100644
index 000000000..168207498
--- /dev/null
+++ b/stubs/livewire/resources/views/livewire/pages/auth/forgot-password.blade.php
@@ -0,0 +1,58 @@
+validate();
+
+ // We will send the password reset link to this user. Once we have attempted
+ // to send the link, we will examine the response then see the message we
+ // need to show to the user. Finally, we'll send out a proper response.
+ $status = Password::sendResetLink(
+ $this->only('email')
+ );
+
+ if ($status != Password::RESET_LINK_SENT) {
+ $this->addError('email', __($status));
+
+ return;
+ }
+
+ $this->reset('email');
+
+ session()->flash('status', __($status));
+ }
+}; ?>
+
+
+
+ {{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }}
+
+
+
+
+
+
+
diff --git a/stubs/livewire/resources/views/livewire/pages/auth/login.blade.php b/stubs/livewire/resources/views/livewire/pages/auth/login.blade.php
new file mode 100644
index 000000000..045f51287
--- /dev/null
+++ b/stubs/livewire/resources/views/livewire/pages/auth/login.blade.php
@@ -0,0 +1,115 @@
+validate();
+
+ $this->ensureIsNotRateLimited();
+
+ if (! auth()->attempt($this->only(['email', 'password'], $this->remember))) {
+ RateLimiter::hit($this->throttleKey());
+
+ throw ValidationException::withMessages([
+ 'email' => trans('auth.failed'),
+ ]);
+ }
+
+ RateLimiter::clear($this->throttleKey());
+
+ session()->regenerate();
+
+ $this->redirect(
+ session('url.intended', RouteServiceProvider::HOME),
+ navigate: true
+ );
+ }
+
+ protected function ensureIsNotRateLimited(): void
+ {
+ if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
+ return;
+ }
+
+ event(new Lockout(request()));
+
+ $seconds = RateLimiter::availableIn($this->throttleKey());
+
+ throw ValidationException::withMessages([
+ 'email' => trans('auth.throttle', [
+ 'seconds' => $seconds,
+ 'minutes' => ceil($seconds / 60),
+ ]),
+ ]);
+ }
+
+ protected function throttleKey(): string
+ {
+ return Str::transliterate(Str::lower($this->email).'|'.request()->ip());
+ }
+}; ?>
+
+
diff --git a/stubs/livewire/resources/views/livewire/pages/auth/register.blade.php b/stubs/livewire/resources/views/livewire/pages/auth/register.blade.php
new file mode 100644
index 000000000..9397bfdeb
--- /dev/null
+++ b/stubs/livewire/resources/views/livewire/pages/auth/register.blade.php
@@ -0,0 +1,85 @@
+validate([
+ 'name' => ['required', 'string', 'max:255'],
+ 'email' => ['required', 'string', 'email', 'max:255', 'unique:'.User::class],
+ 'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()],
+ ]);
+
+ $validated['password'] = Hash::make($validated['password']);
+
+ event(new Registered($user = User::create($validated)));
+
+ auth()->login($user);
+
+ $this->redirect(RouteServiceProvider::HOME, navigate: true);
+ }
+}; ?>
+
+
diff --git a/stubs/livewire/resources/views/livewire/pages/auth/reset-password.blade.php b/stubs/livewire/resources/views/livewire/pages/auth/reset-password.blade.php
new file mode 100644
index 000000000..236c2345a
--- /dev/null
+++ b/stubs/livewire/resources/views/livewire/pages/auth/reset-password.blade.php
@@ -0,0 +1,98 @@
+token = $token;
+
+ $this->email = request()->string('email');
+ }
+
+ public function resetPassword(): void
+ {
+ $this->validate([
+ 'token' => ['required'],
+ 'email' => ['required', 'string', 'email'],
+ 'password' => ['required', 'string', 'confirmed', Rules\Password::defaults()],
+ ]);
+
+ // Here we will attempt to reset the user's password. If it is successful we
+ // will update the password on an actual user model and persist it to the
+ // database. Otherwise we will parse the error and return the response.
+ $status = Password::reset(
+ $this->only('email', 'password', 'password_confirmation', 'token'),
+ function ($user) {
+ $user->forceFill([
+ 'password' => Hash::make($this->password),
+ 'remember_token' => Str::random(60),
+ ])->save();
+
+ event(new PasswordReset($user));
+ }
+ );
+
+ // If the password was successfully reset, we will redirect the user back to
+ // the application's home authenticated view. If there is an error we can
+ // redirect them back to where they came from with their error message.
+ if ($status != Password::PASSWORD_RESET) {
+ $this->addError('email', __($status));
+
+ return;
+ }
+
+ session()->flash('status', __($status));
+
+ $this->redirectRoute('login', navigate: true);
+ }
+}; ?>
+
+
diff --git a/stubs/livewire/resources/views/livewire/pages/auth/verify-email.blade.php b/stubs/livewire/resources/views/livewire/pages/auth/verify-email.blade.php
new file mode 100644
index 000000000..06f108c86
--- /dev/null
+++ b/stubs/livewire/resources/views/livewire/pages/auth/verify-email.blade.php
@@ -0,0 +1,55 @@
+user()->hasVerifiedEmail()) {
+ $this->redirect(
+ session('url.intended', RouteServiceProvider::HOME),
+ navigate: true
+ );
+
+ return;
+ }
+
+ auth()->user()->sendEmailVerificationNotification();
+
+ session()->flash('status', 'verification-link-sent');
+ }
+
+ public function logout(): void
+ {
+ auth()->guard('web')->logout();
+
+ session()->invalidate();
+ session()->regenerateToken();
+
+ $this->redirect('/', navigate: true);
+ }
+}; ?>
+
+
+
+ {{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }}
+
+
+ @if (session('status') == 'verification-link-sent')
+
+ {{ __('A new verification link has been sent to the email address you provided during registration.') }}
+
+ @endif
+
+
+
+ {{ __('Resend Verification Email') }}
+
+
+
+ {{ __('Log Out') }}
+
+
+
diff --git a/stubs/livewire/resources/views/livewire/profile/delete-user-form.blade.php b/stubs/livewire/resources/views/livewire/profile/delete-user-form.blade.php
new file mode 100644
index 000000000..6f0f5f23d
--- /dev/null
+++ b/stubs/livewire/resources/views/livewire/profile/delete-user-form.blade.php
@@ -0,0 +1,77 @@
+validate();
+
+ tap(auth()->user(), fn () => auth()->logout())->delete();
+
+ session()->invalidate();
+ session()->regenerateToken();
+
+ $this->redirect('/', navigate: true);
+ }
+}; ?>
+
+
+
+
+ {{ __('Delete Account') }}
+
+
+
+ {{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.') }}
+
+
+
+ {{ __('Delete Account') }}
+
+
+
+
+
diff --git a/stubs/livewire/resources/views/livewire/profile/update-password-form.blade.php b/stubs/livewire/resources/views/livewire/profile/update-password-form.blade.php
new file mode 100644
index 000000000..d4df3fed2
--- /dev/null
+++ b/stubs/livewire/resources/views/livewire/profile/update-password-form.blade.php
@@ -0,0 +1,75 @@
+validate([
+ 'current_password' => ['required', 'string', 'current_password'],
+ 'password' => ['required', 'string', Password::defaults(), 'confirmed'],
+ ]);
+ } catch (ValidationException $e) {
+ $this->reset('current_password', 'password', 'password_confirmation');
+
+ throw $e;
+ }
+
+ auth()->user()->update([
+ 'password' => Hash::make($validated['password']),
+ ]);
+
+ $this->reset('current_password', 'password', 'password_confirmation');
+
+ $this->dispatch('password-updated');
+ }
+}; ?>
+
+
diff --git a/stubs/livewire/resources/views/livewire/profile/update-profile-information-form.blade.php b/stubs/livewire/resources/views/livewire/profile/update-profile-information-form.blade.php
new file mode 100644
index 000000000..8f2829668
--- /dev/null
+++ b/stubs/livewire/resources/views/livewire/profile/update-profile-information-form.blade.php
@@ -0,0 +1,108 @@
+name = auth()->user()->name;
+ $this->email = auth()->user()->email;
+ }
+
+ public function updateProfileInformation(): void
+ {
+ $user = auth()->user();
+
+ $validated = $this->validate([
+ 'name' => ['required', 'string', 'max:255'],
+ 'email' => ['required', 'email', 'max:255', Rule::unique(User::class)->ignore($user->id)],
+ ]);
+
+ $user->fill($validated);
+
+ if ($user->isDirty('email')) {
+ $user->email_verified_at = null;
+ }
+
+ $user->save();
+
+ $this->dispatch('profile-updated', name: $user->name);
+ }
+
+ public function sendVerification(): void
+ {
+ $user = auth()->user();
+
+ if ($user->hasVerifiedEmail()) {
+ $path = session('url.intended', RouteServiceProvider::HOME);
+
+ $this->redirect($path);
+
+ return;
+ }
+
+ $user->sendEmailVerificationNotification();
+
+ session()->flash('status', 'verification-link-sent');
+ }
+}; ?>
+
+
diff --git a/stubs/livewire/resources/views/livewire/welcome/navigation.blade.php b/stubs/livewire/resources/views/livewire/welcome/navigation.blade.php
new file mode 100644
index 000000000..4ee37bbbe
--- /dev/null
+++ b/stubs/livewire/resources/views/livewire/welcome/navigation.blade.php
@@ -0,0 +1,11 @@
+
+ @auth
+
Dashboard
+ @else
+
Log in
+
+ @if (Route::has('register'))
+
Register
+ @endif
+ @endauth
+
diff --git a/stubs/livewire/resources/views/profile.blade.php b/stubs/livewire/resources/views/profile.blade.php
new file mode 100644
index 000000000..b14bcc1b9
--- /dev/null
+++ b/stubs/livewire/resources/views/profile.blade.php
@@ -0,0 +1,29 @@
+
+
+
+ {{ __('Profile') }}
+
+
+
+
+
diff --git a/stubs/livewire/resources/views/welcome.blade.php b/stubs/livewire/resources/views/welcome.blade.php
new file mode 100644
index 000000000..73666adb5
--- /dev/null
+++ b/stubs/livewire/resources/views/welcome.blade.php
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+ Laravel
+
+
+
+
+
+
+ @vite(['resources/css/app.css', 'resources/js/app.js'])
+
+
+
+ @if (Route::has('login'))
+
+ @endif
+
+
+
+
+
+
+
+
+
+
+
Documentation
+
+
+ Laravel has wonderful documentation covering every aspect of the framework. Whether you are a newcomer or have prior experience with Laravel, we recommend reading our documentation from beginning to end.
+
+
+
+
+
+
+
+
+
+
+
+
+
Laracasts
+
+
+ Laracasts offers thousands of video tutorials on Laravel, PHP, and JavaScript development. Check them out, see for yourself, and massively level up your development skills in the process.
+
+
+
+
+
+
+
+
+
+
+
+
+
Laravel News
+
+
+ Laravel News is a community driven portal and newsletter aggregating all of the latest and most important news in the Laravel ecosystem, including new package releases and tutorials.
+
+
+
+
+
+
+
+
+
+
+
+
+
Vibrant Ecosystem
+
+
+ Laravel's robust library of first-party tools and libraries, such as Forge , Vapor , Nova , and Envoyer help you take your projects to the next level. Pair them with powerful open source libraries like Cashier , Dusk , Echo , Horizon , Sanctum , Telescope , and more.
+
+
+
+
+
+
+
+
+
+
+ Laravel v{{ Illuminate\Foundation\Application::VERSION }} (PHP v{{ PHP_VERSION }})
+
+
+
+
+
+
diff --git a/stubs/livewire/routes/auth.php b/stubs/livewire/routes/auth.php
new file mode 100644
index 000000000..131252e73
--- /dev/null
+++ b/stubs/livewire/routes/auth.php
@@ -0,0 +1,31 @@
+group(function () {
+ Volt::route('register', 'pages.auth.register')
+ ->name('register');
+
+ Volt::route('login', 'pages.auth.login')
+ ->name('login');
+
+ Volt::route('forgot-password', 'pages.auth.forgot-password')
+ ->name('password.request');
+
+ Volt::route('reset-password/{token}', 'pages.auth.reset-password')
+ ->name('password.reset');
+});
+
+Route::middleware('auth')->group(function () {
+ Volt::route('verify-email', 'pages.auth.verify-email')
+ ->name('verification.notice');
+
+ Route::get('verify-email/{id}/{hash}', VerifyEmailController::class)
+ ->middleware(['signed', 'throttle:6,1'])
+ ->name('verification.verify');
+
+ Volt::route('confirm-password', 'pages.auth.confirm-password')
+ ->name('password.confirm');
+});
diff --git a/stubs/livewire/routes/web.php b/stubs/livewire/routes/web.php
new file mode 100644
index 000000000..cd900f1aa
--- /dev/null
+++ b/stubs/livewire/routes/web.php
@@ -0,0 +1,26 @@
+middleware(['auth', 'verified'])
+ ->name('dashboard');
+
+Route::view('profile', 'profile')
+ ->middleware(['auth'])
+ ->name('profile');
+
+require __DIR__.'/auth.php';
diff --git a/stubs/livewire/tests/Feature/Auth/AuthenticationTest.php b/stubs/livewire/tests/Feature/Auth/AuthenticationTest.php
new file mode 100644
index 000000000..8fde170f5
--- /dev/null
+++ b/stubs/livewire/tests/Feature/Auth/AuthenticationTest.php
@@ -0,0 +1,87 @@
+get('/login');
+
+ $response
+ ->assertSeeVolt('pages.auth.login')
+ ->assertOk();
+ }
+
+ public function test_users_can_authenticate_using_the_login_screen(): void
+ {
+ $user = User::factory()->create();
+
+ $component = Volt::test('pages.auth.login')
+ ->set('email', $user->email)
+ ->set('password', 'password');
+
+ $component->call('login');
+
+ $component
+ ->assertHasNoErrors()
+ ->assertRedirect(RouteServiceProvider::HOME);
+
+ $this->assertAuthenticated();
+ }
+
+ public function test_users_can_not_authenticate_with_invalid_password(): void
+ {
+ $user = User::factory()->create();
+
+ $component = Volt::test('pages.auth.login')
+ ->set('email', $user->email)
+ ->set('password', 'wrong-password');
+
+ $component->call('login');
+
+ $component
+ ->assertHasErrors()
+ ->assertNoRedirect();
+
+ $this->assertGuest();
+ }
+
+ public function test_navigation_menu_can_be_rendered(): void
+ {
+ $user = User::factory()->create();
+
+ $this->actingAs($user);
+
+ $response = $this->get('/dashboard');
+
+ $response
+ ->assertSeeVolt('layout.navigation')
+ ->assertOk();
+ }
+
+ public function test_users_can_logout(): void
+ {
+ $user = User::factory()->create();
+
+ $this->actingAs($user);
+
+ $component = Volt::test('layout.navigation');
+
+ $component->call('logout');
+
+ $component
+ ->assertHasNoErrors()
+ ->assertRedirect('/');
+
+ $this->assertGuest();
+ }
+}
diff --git a/stubs/livewire/tests/Feature/Auth/EmailVerificationTest.php b/stubs/livewire/tests/Feature/Auth/EmailVerificationTest.php
new file mode 100644
index 000000000..cfd4d6afa
--- /dev/null
+++ b/stubs/livewire/tests/Feature/Auth/EmailVerificationTest.php
@@ -0,0 +1,67 @@
+create([
+ 'email_verified_at' => null,
+ ]);
+
+ $response = $this->actingAs($user)->get('/verify-email');
+
+ $response
+ ->assertSeeVolt('pages.auth.verify-email')
+ ->assertStatus(200);
+ }
+
+ public function test_email_can_be_verified(): void
+ {
+ $user = User::factory()->create([
+ 'email_verified_at' => null,
+ ]);
+
+ 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(RouteServiceProvider::HOME.'?verified=1');
+ }
+
+ public function test_email_is_not_verified_with_invalid_hash(): void
+ {
+ $user = User::factory()->create([
+ 'email_verified_at' => null,
+ ]);
+
+ $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());
+ }
+}
diff --git a/stubs/livewire/tests/Feature/Auth/PasswordConfirmationTest.php b/stubs/livewire/tests/Feature/Auth/PasswordConfirmationTest.php
new file mode 100644
index 000000000..e91c6bb0e
--- /dev/null
+++ b/stubs/livewire/tests/Feature/Auth/PasswordConfirmationTest.php
@@ -0,0 +1,56 @@
+create();
+
+ $response = $this->actingAs($user)->get('/confirm-password');
+
+ $response
+ ->assertSeeVolt('pages.auth.confirm-password')
+ ->assertStatus(200);
+ }
+
+ public function test_password_can_be_confirmed(): void
+ {
+ $user = User::factory()->create();
+
+ $this->actingAs($user);
+
+ $component = Volt::test('pages.auth.confirm-password')
+ ->set('password', 'password');
+
+ $component->call('confirmPassword');
+
+ $component
+ ->assertRedirect('/dashboard')
+ ->assertHasNoErrors();
+ }
+
+ public function test_password_is_not_confirmed_with_invalid_password(): void
+ {
+ $user = User::factory()->create();
+
+ $this->actingAs($user);
+
+ $component = Volt::test('pages.auth.confirm-password')
+ ->set('password', 'wrong-password');
+
+ $component->call('confirmPassword');
+
+ $component
+ ->assertNoRedirect()
+ ->assertHasErrors('password');
+ }
+}
diff --git a/stubs/livewire/tests/Feature/Auth/PasswordResetTest.php b/stubs/livewire/tests/Feature/Auth/PasswordResetTest.php
new file mode 100644
index 000000000..f5bc76514
--- /dev/null
+++ b/stubs/livewire/tests/Feature/Auth/PasswordResetTest.php
@@ -0,0 +1,84 @@
+get('/forgot-password');
+
+ $response
+ ->assertSeeVolt('pages.auth.forgot-password')
+ ->assertStatus(200);
+ }
+
+ public function test_reset_password_link_can_be_requested(): void
+ {
+ Notification::fake();
+
+ $user = User::factory()->create();
+
+ Volt::test('pages.auth.forgot-password')
+ ->set('email', $user->email)
+ ->call('sendPasswordResetLink');
+
+ Notification::assertSentTo($user, ResetPassword::class);
+ }
+
+ public function test_reset_password_screen_can_be_rendered(): void
+ {
+ Notification::fake();
+
+ $user = User::factory()->create();
+
+ Volt::test('pages.auth.forgot-password')
+ ->set('email', $user->email)
+ ->call('sendPasswordResetLink');
+
+ Notification::assertSentTo($user, ResetPassword::class, function ($notification) {
+ $response = $this->get('/reset-password/'.$notification->token);
+
+ $response
+ ->assertSeeVolt('pages.auth.reset-password')
+ ->assertStatus(200);
+
+ return true;
+ });
+ }
+
+ public function test_password_can_be_reset_with_valid_token(): void
+ {
+ Notification::fake();
+
+ $user = User::factory()->create();
+
+ Volt::test('pages.auth.forgot-password')
+ ->set('email', $user->email)
+ ->call('sendPasswordResetLink');
+
+ Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
+ $component = Volt::test('pages.auth.reset-password', ['token' => $notification->token])
+ ->set('email', $user->email)
+ ->set('password', 'password')
+ ->set('password_confirmation', 'password');
+
+ $component->call('resetPassword');
+
+ $component
+ ->assertRedirect('/login')
+ ->assertHasNoErrors();
+
+ return true;
+ });
+ }
+}
diff --git a/stubs/livewire/tests/Feature/Auth/PasswordUpdateTest.php b/stubs/livewire/tests/Feature/Auth/PasswordUpdateTest.php
new file mode 100644
index 000000000..d24c29589
--- /dev/null
+++ b/stubs/livewire/tests/Feature/Auth/PasswordUpdateTest.php
@@ -0,0 +1,50 @@
+create();
+
+ $this->actingAs($user);
+
+ $component = Volt::test('profile.update-password-form')
+ ->set('current_password', 'password')
+ ->set('password', 'new-password')
+ ->set('password_confirmation', 'new-password')
+ ->call('updatePassword');
+
+ $component
+ ->assertHasNoErrors()
+ ->assertNoRedirect();
+
+ $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();
+
+ $this->actingAs($user);
+
+ $component = Volt::test('profile.update-password-form')
+ ->set('current_password', 'wrong-password')
+ ->set('password', 'new-password')
+ ->set('password_confirmation', 'new-password')
+ ->call('updatePassword');
+
+ $component
+ ->assertHasErrors(['current_password'])
+ ->assertNoRedirect();
+ }
+}
diff --git a/stubs/livewire/tests/Feature/Auth/RegistrationTest.php b/stubs/livewire/tests/Feature/Auth/RegistrationTest.php
new file mode 100644
index 000000000..91ade8c58
--- /dev/null
+++ b/stubs/livewire/tests/Feature/Auth/RegistrationTest.php
@@ -0,0 +1,37 @@
+get('/register');
+
+ $response
+ ->assertSeeVolt('pages.auth.register')
+ ->assertOk();
+ }
+
+ public function test_new_users_can_register(): void
+ {
+ $component = Volt::test('pages.auth.register')
+ ->set('name', 'Test User')
+ ->set('email', 'test@example.com')
+ ->set('password', 'password')
+ ->set('password_confirmation', 'password');
+
+ $component->call('register');
+
+ $component->assertRedirect(RouteServiceProvider::HOME);
+
+ $this->assertAuthenticated();
+ }
+}
diff --git a/stubs/livewire/tests/Feature/ProfileTest.php b/stubs/livewire/tests/Feature/ProfileTest.php
new file mode 100644
index 000000000..668fa610d
--- /dev/null
+++ b/stubs/livewire/tests/Feature/ProfileTest.php
@@ -0,0 +1,101 @@
+create();
+
+ $response = $this->actingAs($user)->get('/profile');
+
+ $response
+ ->assertSeeVolt('profile.update-profile-information-form')
+ ->assertSeeVolt('profile.update-password-form')
+ ->assertSeeVolt('profile.delete-user-form')
+ ->assertOk();
+ }
+
+ public function test_profile_information_can_be_updated(): void
+ {
+ $user = User::factory()->create();
+
+ $this->actingAs($user);
+
+ $component = Volt::test('profile.update-profile-information-form')
+ ->set('name', 'Test User')
+ ->set('email', 'test@example.com')
+ ->call('updateProfileInformation');
+
+ $component
+ ->assertHasNoErrors()
+ ->assertNoRedirect();
+
+ $user->refresh();
+
+ $this->assertSame('Test User', $user->name);
+ $this->assertSame('test@example.com', $user->email);
+ $this->assertNull($user->email_verified_at);
+ }
+
+ public function test_email_verification_status_is_unchanged_when_the_email_address_is_unchanged(): void
+ {
+ $user = User::factory()->create();
+
+ $this->actingAs($user);
+
+ $component = Volt::test('profile.update-profile-information-form')
+ ->set('name', 'Test User')
+ ->set('email', $user->email)
+ ->call('updateProfileInformation');
+
+ $component
+ ->assertHasNoErrors()
+ ->assertNoRedirect();
+
+ $this->assertNotNull($user->refresh()->email_verified_at);
+ }
+
+ public function test_user_can_delete_their_account(): void
+ {
+ $user = User::factory()->create();
+
+ $this->actingAs($user);
+
+ $component = Volt::test('profile.delete-user-form')
+ ->set('password', 'password')
+ ->call('deleteUser');
+
+ $component
+ ->assertHasNoErrors()
+ ->assertRedirect('/');
+
+ $this->assertGuest();
+ $this->assertNull($user->fresh());
+ }
+
+ public function test_correct_password_must_be_provided_to_delete_account(): void
+ {
+ $user = User::factory()->create();
+
+ $this->actingAs($user);
+
+ $component = Volt::test('profile.delete-user-form')
+ ->set('password', 'wrong-password')
+ ->call('deleteUser');
+
+ $component
+ ->assertHasErrors('password')
+ ->assertNoRedirect();
+
+ $this->assertNotNull($user->fresh());
+ }
+}