Skip to content

Commit

Permalink
[1.x] Adds Livewire stack 🐙 (#314)
Browse files Browse the repository at this point in the history
* Adds basic stacks

* Installs Livewire and Volt

* Displays login

* Keeps working on login

* Adds base register template

* Continues to work on register template

* Fixes password_confirmation

* Replaces home by dashboard

* Keeps working on login / register

* Uses `wire:navigate`

* Keeps working on dashboard

* Fixes view

* Apply fixes from StyleCI

* Uses navigate

* Fixes logout style

* Restructures

* Improves welcome

* Adds wire:navigate to reset password

* Adds wire:navigate to application logo

* Adds reset password

* Adds wire navigate to already registered button

* Adds profile information form

* Style

* Fixes redirect

* Adds email verification

* Adds update password

* Completes update password

* Adds delete user form

* Keeps working on profile forms

* Missing navigates

* Adds confirm password

* Improves code

* Fixes delete account errors

* Adds verify email

* Adjusts install

* Uses process to install volt

* Apply fixes from StyleCI

* Reverts change

* Reverts changes

* Simplifies logout

* Fixes updating name

* Style

* Removes leftovers of "old" function

* Fixes confirm password

* Makes string validation consistent

* Adds profile tests

* Adds more auth tests

* Removes non used variable

* Simplifies acting as

* Adds email verification test

* Adds password confirmation test

* Allows to inject token

* Add password reset test

* Adds password update test

* Fixes name

* Adds registration test

* Updates description

* Uses stable version of Livewire

* Variable order

* Adds missing query parameter

* Reverts

* Spacing

* Ordering

* Simplifies

* Makes name and email required

* Adds `livewire` stack to test matrix

* Update navigation.blade.php

* formatting

* Removes `logged-in` component

* Imports `MustVerifyEmail` contract

* formatting

---------

Co-authored-by: StyleCI Bot <[email protected]>
Co-authored-by: Taylor Otwell <[email protected]>
  • Loading branch information
3 people authored Sep 20, 2023
1 parent 24b2215 commit d3979dc
Show file tree
Hide file tree
Showing 39 changed files with 2,219 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
17 changes: 12 additions & 5 deletions src/Console/InstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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;
}
Expand All @@ -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')) {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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
Expand Down
101 changes: 101 additions & 0 deletions src/Console/InstallsLivewireStack.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

namespace Laravel\Breeze\Console;

use Illuminate\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Process\Process;

trait InstallsLivewireStack
{
/**
* Install the Livewire Breeze stack.
*
* @return int|null
*/
protected function installLivewireStack()
{
// NPM Packages...
$this->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.');
}
}
73 changes: 73 additions & 0 deletions stubs/livewire/pest-tests/Feature/Auth/AuthenticationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

use App\Models\User;
use App\Providers\RouteServiceProvider;
use Livewire\Volt\Volt;

test('login screen can be rendered', function () {
$response = $this->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();
});
53 changes: 53 additions & 0 deletions stubs/livewire/pest-tests/Feature/Auth/EmailVerificationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

use App\Models\User;
use App\Providers\RouteServiceProvider;
use Illuminate\Auth\Events\Verified;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\URL;

test('email verification screen can be rendered', function () {
$user = User::factory()->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();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace Tests\Feature\Auth;

use App\Models\User;
use Livewire\Volt\Volt;

test('confirm password screen can be rendered', function () {
$user = User::factory()->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');
});
Loading

0 comments on commit d3979dc

Please sign in to comment.