feature - 4 - Set up e2e with basic tests #10

Merged
myrmidex merged 1 commit from refs/pull/10/head into release/v0.3 2025-12-28 13:49:27 +01:00
12 changed files with 233 additions and 2 deletions

24
.env.dusk.local Normal file
View file

@ -0,0 +1,24 @@
APP_NAME=DishPlanner
APP_ENV=testing
APP_KEY=base64:KSKZNT+cJuaBRBv4Y2HQqav6hzREKoLkNIKN8yszU1Q=
APP_DEBUG=true
APP_URL=http://dishplanner_app:8000
LOG_CHANNEL=single
# Test database
DB_CONNECTION=mysql
DB_HOST=db
DB_PORT=3306
DB_DATABASE=dishplanner_test
DB_USERNAME=dishplanner
DB_PASSWORD=dishplanner
BROADCAST_DRIVER=log
CACHE_DRIVER=array
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=array
SESSION_LIFETIME=120
MAIL_MAILER=array

View file

@ -17,6 +17,7 @@
}, },
"require-dev": { "require-dev": {
"fakerphp/faker": "^1.23", "fakerphp/faker": "^1.23",
"laravel/dusk": "^8.3",
"laravel/pail": "^1.1", "laravel/pail": "^1.1",
"laravel/pint": "^1.13", "laravel/pint": "^1.13",
"laravel/sail": "^1.26", "laravel/sail": "^1.26",

View file

@ -63,8 +63,8 @@ services:
MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD:-root}" MYSQL_ROOT_PASSWORD: "${DB_ROOT_PASSWORD:-root}"
volumes: volumes:
- db_data:/var/lib/mysql - db_data:/var/lib/mysql
# Optional: Initialize with SQL dump # Initialize with SQL scripts
# - ./database/dumps:/docker-entrypoint-initdb.d - ./docker/mysql-init:/docker-entrypoint-initdb.d
healthcheck: healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s interval: 10s
@ -84,6 +84,21 @@ services:
networks: networks:
- dishplanner - dishplanner
# Selenium for E2E testing with Dusk
selenium:
image: selenium/standalone-chrome:latest
container_name: dishplanner_selenium
restart: unless-stopped
ports:
- "4444:4444" # Selenium server
- "7900:7900" # VNC server for debugging
volumes:
- /dev/shm:/dev/shm
networks:
- dishplanner
environment:
- SE_VNC_PASSWORD=secret
# Optional: Redis for caching/sessions # Optional: Redis for caching/sessions
# redis: # redis:
# image: redis:alpine # image: redis:alpine

View file

@ -0,0 +1,8 @@
-- Create test database for Dusk E2E tests
CREATE DATABASE IF NOT EXISTS dishplanner_test;
-- Grant all privileges on test database to the dishplanner user
GRANT ALL PRIVILEGES ON dishplanner_test.* TO 'dishplanner'@'%' IDENTIFIED BY 'dishplanner';
GRANT ALL PRIVILEGES ON dishplanner_test.* TO 'dishplanner'@'localhost' IDENTIFIED BY 'dishplanner';
FLUSH PRIVILEGES;

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',
];
}
}

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',
];
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace Tests\Browser;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Illuminate\Foundation\Testing\DatabaseTransactions;
class RedirectTest extends DuskTestCase
{
use DatabaseTransactions;
/**
* Test that unauthenticated users are redirected to login
*/
public function testUnauthenticatedRedirectsToLogin()
{
$this->browse(function (Browser $browser) {
$browser->visit('http://dishplanner_app:8000/dashboard')
->assertPathIs('/login')
->assertSee('Login');
});
}
/**
* Test that login page loads correctly
*/
public function testLoginPageLoads()
{
$this->browse(function (Browser $browser) {
$browser->visit('http://dishplanner_app:8000/login')
->assertPathIs('/login')
->assertSee('Login')
->assertSee('Email')
->assertSee('Password');
});
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace Tests\Browser;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use App\Models\Planner;
class RegistrationOnlyTest extends DuskTestCase
{
public function testUserRegistration(): void
{
// Generate unique test data with timestamp to avoid conflicts
$timestamp = now()->format('YmdHis');
$testData = [
'name' => "Test User {$timestamp}",
'email' => "test.{$timestamp}@example.com",
'password' => 'SecurePassword123!',
];
$this->browse(function (Browser $browser) use ($testData) {
$browser->visit('http://dishplanner_app:8000/register')
->waitFor('input[id="name"]', 5)
->type('input[id="name"]', $testData['name'])
->type('input[id="email"]', $testData['email'])
->type('input[id="password"]', $testData['password'])
->type('input[id="password_confirmation"]', $testData['password'])
->screenshot('filled-form')
->click('button[type="submit"]')
->pause(3000) // Give more time for processing
->screenshot('after-submit')
->assertSee("Welcome {$testData['name']}!") // Verify successful registration and login
->assertPathIs('/dashboard'); // Should be on dashboard
});
}
}

2
tests/Browser/console/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*
!.gitignore

2
tests/Browser/screenshots/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*
!.gitignore

2
tests/Browser/source/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*
!.gitignore

47
tests/DuskTestCase.php Normal file
View file

@ -0,0 +1,47 @@
<?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
{
// Don't start ChromeDriver - we're using Selenium
}
/**
* Create the RemoteWebDriver instance.
*/
protected function driver(): RemoteWebDriver
{
$options = (new ChromeOptions)->addArguments([
'--window-size=1920,1080',
'--no-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
'--headless=new',
'--disable-extensions',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
]);
return RemoteWebDriver::create(
'http://selenium:4444/wd/hub', // Connect to Selenium container
DesiredCapabilities::chrome()->setCapability(
ChromeOptions::CAPABILITY, $options
)
);
}
}