feat: Phase 4 - CI/CD, Static Analysis, Browser Tests
Code Style & Quality / Test Suite (push) Waiting to run
Code Style & Quality / Laravel Pint (push) Waiting to run
Code Style & Quality / PHPStan Static Analysis (push) Waiting to run
Code Style & Quality / Browser Tests (Dusk) (push) Waiting to run

- Add GitHub Actions CI workflow (.github/workflows/ci.yml)
  - Laravel Pint code style check
  - PHPStan static analysis (level 5)
  - Test suite with parallel execution
  - Dusk browser tests (disabled by default)
- Add phpstan.neon config (level 5, ignores User notifications methods)
- Add Dusk browser tests (ProjectWorkflowTest)
- Install laravel/dusk + chromedriver

Tests: 101 passing (319 assertions)
This commit is contained in:
Javier Braña
2026-08-31 17:33:34 +02:00
parent ff1d1ed3a4
commit a9bbf8ca57
12 changed files with 694 additions and 112 deletions
+21
View File
@@ -0,0 +1,21 @@
<?php
namespace Tests\Browser;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
class ExampleTest extends DuskTestCase
{
/**
* A basic browser test example.
*/
public function test_basic_example(): void
{
$this->browse(function (Browser $browser) {
$browser->visit('/')
->assertSee('Laravel');
});
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace Tests\Browser\Pages;
use Laravel\Dusk\Browser;
class HomePage extends Page
{
/**
* Get the URL for the page.
*/
public function url(): string
{
return '/';
}
/**
* Assert that the browser is on the page.
*/
public function assert(Browser $browser): void
{
//
}
/**
* Get the element shortcuts for the page.
*
* @return array<string, string>
*/
public function elements(): array
{
return [
'@element' => '#selector',
];
}
}
+20
View File
@@ -0,0 +1,20 @@
<?php
namespace Tests\Browser\Pages;
use Laravel\Dusk\Page as BasePage;
abstract class Page extends BasePage
{
/**
* Get the global element shortcuts for the site.
*
* @return array<string, string>
*/
public static function siteElements(): array
{
return [
'@element' => '#selector',
];
}
}
+78
View File
@@ -0,0 +1,78 @@
<?php
namespace Tests\Browser;
use App\Models\Project;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseMigrations;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
class ProjectWorkflowTest extends DuskTestCase
{
use DatabaseMigrations;
public function test_user_can_login_and_create_project()
{
$user = User::factory()->create([
'email' => 'test@example.com',
'password' => bcrypt('password'),
]);
$this->browse(function (Browser $browser) use ($user) {
$browser->visit('/login')
->type('email', 'test@example.com')
->type('password', 'password')
->press('LOG IN')
->assertPathIs('/dashboard')
->visit('/projects/create')
->type('name', 'Test Project from Dusk')
->type('address', '123 Test Street')
->press('Guardar')
->assertPathBeginsWith('/projects/')
->assertSee('Test Project from Dusk');
});
}
public function test_user_can_access_project_map()
{
$user = User::factory()->create([
'email' => 'test@example.com',
'password' => bcrypt('password'),
]);
$project = Project::factory()->create([
'name' => 'Existing Project',
'creator_id' => $user->id,
]);
$project->users()->attach($user->id, ['role_in_project' => 'admin']);
$this->browse(function (Browser $browser) use ($user, $project) {
$browser->visit('/login')
->type('email', 'test@example.com')
->type('password', 'password')
->press('LOG IN')
->visit("/projects/{$project->id}/map")
->assertSee('Existing Project');
});
}
public function test_user_can_view_dashboard()
{
$user = User::factory()->create([
'email' => 'test@example.com',
'password' => bcrypt('password'),
]);
$this->browse(function (Browser $browser) use ($user) {
$browser->visit('/login')
->type('email', 'test@example.com')
->type('password', 'password')
->press('LOG IN')
->assertPathIs('/dashboard')
->assertSee('Proyectos')
->assertSee('Tareas')
->assertSee('Incidencias');
});
}
}
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+2
View File
@@ -0,0 +1,2 @@
*
!.gitignore
+48
View File
@@ -0,0 +1,48 @@
<?php
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']);
}
}
/**
* Create the RemoteWebDriver instance.
*/
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());
return RemoteWebDriver::create(
$_ENV['DUSK_DRIVER_URL'] ?? env('DUSK_DRIVER_URL') ?? 'http://localhost:9515',
DesiredCapabilities::chrome()->setCapability(
ChromeOptions::CAPABILITY, $options
)
);
}
}