feature - 8 - Fix scheduled user dishes

This commit is contained in:
myrmidex 2025-12-29 23:36:15 +01:00
parent 09236f6f10
commit b93e6cb832
84 changed files with 3471 additions and 752 deletions

View file

@ -2,8 +2,14 @@
namespace App\Livewire\Schedule;
use App\Models\ScheduledUserDish;
use App\Models\User;
use Carbon\Carbon;
use DishPlanner\Schedule\Services\ScheduleCalendarService;
use DishPlanner\ScheduledUserDish\Actions\DeleteScheduledUserDishForDateAction;
use DishPlanner\ScheduledUserDish\Actions\SkipScheduledUserDishForDateAction;
use Exception;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Log;
use Livewire\Component;
class ScheduleCalendar extends Component
@ -14,65 +20,37 @@ class ScheduleCalendar extends Component
public $showRegenerateModal = false;
public $regenerateDate = null;
public $regenerateUserId = null;
public function mount()
public function mount(): void
{
$this->currentMonth = now()->month;
$this->currentYear = now()->year;
$this->generateCalendar();
$this->loadCalendar();
}
protected $listeners = ['schedule-generated' => 'refreshCalendar'];
public function render()
public function render(): View
{
return view('livewire.schedule.schedule-calendar');
}
public function refreshCalendar()
public function refreshCalendar(): void
{
$this->generateCalendar();
$this->loadCalendar();
}
public function generateCalendar()
public function loadCalendar(): void
{
$this->calendarDays = [];
// Get first day of the month and total days
$firstDay = Carbon::createFromDate($this->currentYear, $this->currentMonth, 1);
$daysInMonth = $firstDay->daysInMonth;
// Generate 31 days for consistency with React version
for ($day = 1; $day <= 31; $day++) {
if ($day <= $daysInMonth) {
$date = Carbon::createFromDate($this->currentYear, $this->currentMonth, $day);
// Get scheduled dishes for this date
$scheduledDishes = ScheduledUserDish::with(['user', 'dish'])
->whereDate('date', $date->format('Y-m-d'))
->get();
$this->calendarDays[] = [
'day' => $day,
'date' => $date,
'isToday' => $date->isToday(),
'scheduledDishes' => $scheduledDishes,
'isEmpty' => $scheduledDishes->isEmpty()
];
} else {
// Empty slot for days that don't exist in this month
$this->calendarDays[] = [
'day' => null,
'date' => null,
'isToday' => false,
'scheduledDishes' => collect(),
'isEmpty' => true
];
}
}
$service = new ScheduleCalendarService();
$this->calendarDays = $service->getCalendarDays(
auth()->user(),
$this->currentMonth,
$this->currentYear
);
}
public function previousMonth()
public function previousMonth(): void
{
if ($this->currentMonth === 1) {
$this->currentMonth = 12;
@ -80,10 +58,10 @@ public function previousMonth()
} else {
$this->currentMonth--;
}
$this->generateCalendar();
$this->loadCalendar();
}
public function nextMonth()
public function nextMonth(): void
{
if ($this->currentMonth === 12) {
$this->currentMonth = 1;
@ -91,62 +69,86 @@ public function nextMonth()
} else {
$this->currentMonth++;
}
$this->generateCalendar();
$this->loadCalendar();
}
public function regenerateForUserDate($date, $userId)
public function regenerateForUserDate($date, $userId): void
{
if (!$this->authorizeUser($userId)) {
session()->flash('error', 'Unauthorized action.');
return;
}
$this->regenerateDate = $date;
$this->regenerateUserId = $userId;
$this->showRegenerateModal = true;
}
public function confirmRegenerate()
public function confirmRegenerate(): void
{
try {
// Delete existing scheduled dish for this user on this date
ScheduledUserDish::whereDate('date', $this->regenerateDate)
->where('user_id', $this->regenerateUserId)
->delete();
// You could call a specific regeneration method here
// For now, we'll just delete and let the user generate again
if (!$this->authorizeUser($this->regenerateUserId)) {
session()->flash('error', 'Unauthorized action.');
return;
}
$action = new DeleteScheduledUserDishForDateAction();
$action->execute(
auth()->user(),
Carbon::parse($this->regenerateDate),
$this->regenerateUserId
);
$this->showRegenerateModal = false;
$this->generateCalendar(); // Refresh calendar
$this->loadCalendar();
session()->flash('success', 'Schedule regenerated for the selected date!');
} catch (\Exception $e) {
session()->flash('error', 'Error regenerating schedule: ' . $e->getMessage());
} catch (Exception $e) {
Log::error('Schedule regeneration failed', ['exception' => $e, 'date' => $this->regenerateDate]);
session()->flash('error', 'Unable to regenerate schedule. Please try again.');
}
}
public function skipDay($date, $userId)
public function skipDay($date, $userId): void
{
try {
// Mark this day as skipped or delete the assignment
ScheduledUserDish::whereDate('date', $date)
->where('user_id', $userId)
->delete();
$this->generateCalendar(); // Refresh calendar
if (!$this->authorizeUser($userId)) {
session()->flash('error', 'Unauthorized action.');
return;
}
$action = new SkipScheduledUserDishForDateAction();
$action->execute(
auth()->user(),
Carbon::parse($date),
$userId
);
$this->loadCalendar();
session()->flash('success', 'Day skipped successfully!');
} catch (\Exception $e) {
session()->flash('error', 'Error skipping day: ' . $e->getMessage());
} catch (Exception $e) {
Log::error('Skip day failed', ['exception' => $e, 'date' => $date, 'userId' => $userId]);
session()->flash('error', 'Unable to skip day. Please try again.');
}
}
public function cancel()
private function authorizeUser(int $userId): bool
{
$user = User::find($userId);
return $user && $user->planner_id === auth()->id();
}
public function cancel(): void
{
$this->showRegenerateModal = false;
$this->regenerateDate = null;
$this->regenerateUserId = null;
}
public function getMonthNameProperty()
public function getMonthNameProperty(): string
{
return Carbon::createFromDate($this->currentYear, $this->currentMonth, 1)->format('F Y');
$service = new ScheduleCalendarService();
return $service->getMonthName($this->currentMonth, $this->currentYear);
}
}
}

View file

@ -3,184 +3,120 @@
namespace App\Livewire\Schedule;
use App\Models\User;
use App\Models\Dish;
use App\Models\Schedule;
use App\Models\ScheduledUserDish;
use Carbon\Carbon;
use DishPlanner\Schedule\Actions\ClearScheduleForMonthAction;
use DishPlanner\Schedule\Actions\GenerateScheduleForMonthAction;
use DishPlanner\Schedule\Actions\RegenerateScheduleForDateForUsersAction;
use Illuminate\Support\Facades\Log;
use Livewire\Component;
class ScheduleGenerator extends Component
{
private const YEARS_IN_PAST = 1;
private const YEARS_IN_FUTURE = 5;
public $selectedMonth;
public $selectedYear;
public $selectedUsers = [];
public $clearExisting = true;
public $showAdvancedOptions = false;
public $isGenerating = false;
public function mount()
public function mount(): void
{
$this->selectedMonth = now()->month;
$this->selectedYear = now()->year;
// Select all users by default
$this->selectedUsers = User::where('planner_id', auth()->id())
->pluck('id')
->toArray();
}
public function render()
public function render(): \Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
{
$users = User::where('planner_id', auth()->id())
->orderBy('name')
->get();
$months = [
1 => 'January', 2 => 'February', 3 => 'March', 4 => 'April',
5 => 'May', 6 => 'June', 7 => 'July', 8 => 'August',
9 => 'September', 10 => 'October', 11 => 'November', 12 => 'December'
];
$years = range(now()->year - 1, now()->year + 2);
$years = range(now()->year - self::YEARS_IN_PAST, now()->year + self::YEARS_IN_FUTURE);
return view('livewire.schedule.schedule-generator', [
'users' => $users,
'months' => $months,
'months' => $this->getMonthNames(),
'years' => $years
]);
}
public function generate()
public function generate(): void
{
$this->validate([
'selectedUsers' => 'required|array|min:1',
'selectedMonth' => 'required|integer|min:1|max:12',
'selectedYear' => 'required|integer|min:2020|max:2030',
'selectedYear' => 'required|integer|min:' . (now()->year - self::YEARS_IN_PAST) . '|max:' . (now()->year + self::YEARS_IN_FUTURE),
]);
$this->isGenerating = true;
try {
$startDate = Carbon::createFromDate($this->selectedYear, $this->selectedMonth, 1);
$endDate = $startDate->copy()->endOfMonth();
// Clear existing schedule if requested
if ($this->clearExisting) {
ScheduledUserDish::whereBetween('date', [$startDate, $endDate])
->whereIn('user_id', $this->selectedUsers)
->delete();
}
// Get all dishes assigned to selected users
$userDishes = [];
foreach ($this->selectedUsers as $userId) {
$user = User::find($userId);
$dishes = $user->dishes()->get();
if ($dishes->isNotEmpty()) {
$userDishes[$userId] = $dishes->toArray();
}
}
// Generate schedule for each day
$currentDate = $startDate->copy();
while ($currentDate <= $endDate) {
foreach ($this->selectedUsers as $userId) {
// Skip if user already has a dish for this day
if (ScheduledUserDish::where('date', $currentDate->format('Y-m-d'))
->where('user_id', $userId)
->exists()) {
continue;
}
// Get available dishes for this user
if (!isset($userDishes[$userId]) || empty($userDishes[$userId])) {
continue;
}
$availableDishes = $userDishes[$userId];
// Simple random assignment (you can implement more complex logic here)
if (!empty($availableDishes)) {
$randomDish = $availableDishes[array_rand($availableDishes)];
ScheduledUserDish::create([
'user_id' => $userId,
'dish_id' => $randomDish['id'],
'date' => $currentDate->format('Y-m-d'),
'planner_id' => auth()->id(),
]);
}
}
$currentDate->addDay();
}
$action = new GenerateScheduleForMonthAction();
$action->execute(
auth()->user(),
$this->selectedMonth,
$this->selectedYear,
$this->selectedUsers,
$this->clearExisting
);
$this->isGenerating = false;
// Emit event to refresh calendar
$this->dispatch('schedule-generated');
session()->flash('success', 'Schedule generated successfully for ' .
session()->flash('success', 'Schedule generated successfully for ' .
$this->getSelectedMonthName() . ' ' . $this->selectedYear);
} catch (\Exception $e) {
$this->isGenerating = false;
session()->flash('error', 'Error generating schedule: ' . $e->getMessage());
Log::error('Schedule generation failed', ['exception' => $e]);
session()->flash('error', 'Unable to generate schedule. Please try again.');
}
}
public function regenerateForDate($date)
public function regenerateForDate($date): void
{
try {
// Clear existing assignments for this date
ScheduledUserDish::whereDate('date', $date)
->whereIn('user_id', $this->selectedUsers)
->delete();
// Regenerate for this specific date
$currentDate = Carbon::parse($date);
foreach ($this->selectedUsers as $userId) {
$user = User::find($userId);
$dishes = $user->dishes()->get();
if ($dishes->isNotEmpty()) {
$randomDish = $dishes->random();
ScheduledUserDish::create([
'user_id' => $userId,
'dish_id' => $randomDish->id,
'date' => $currentDate->format('Y-m-d'),
'planner_id' => auth()->id(),
]);
}
}
$action = new RegenerateScheduleForDateForUsersAction();
$action->execute(
auth()->user(),
Carbon::parse($date),
$this->selectedUsers
);
$this->dispatch('schedule-generated');
session()->flash('success', 'Schedule regenerated for ' . $currentDate->format('M d, Y'));
session()->flash('success', 'Schedule regenerated for ' . Carbon::parse($date)->format('M d, Y'));
} catch (\Exception $e) {
session()->flash('error', 'Error regenerating schedule: ' . $e->getMessage());
Log::error('Schedule regeneration failed', ['exception' => $e, 'date' => $date]);
session()->flash('error', 'Unable to regenerate schedule. Please try again.');
}
}
public function clearMonth()
public function clearMonth(): void
{
try {
$startDate = Carbon::createFromDate($this->selectedYear, $this->selectedMonth, 1);
$endDate = $startDate->copy()->endOfMonth();
ScheduledUserDish::whereBetween('date', [$startDate, $endDate])
->whereIn('user_id', $this->selectedUsers)
->delete();
$action = new ClearScheduleForMonthAction();
$action->execute(
auth()->user(),
$this->selectedMonth,
$this->selectedYear,
$this->selectedUsers
);
$this->dispatch('schedule-generated');
session()->flash('success', 'Schedule cleared for ' .
session()->flash('success', 'Schedule cleared for ' .
$this->getSelectedMonthName() . ' ' . $this->selectedYear);
} catch (\Exception $e) {
session()->flash('error', 'Error clearing schedule: ' . $e->getMessage());
Log::error('Clear month failed', ['exception' => $e]);
session()->flash('error', 'Unable to clear schedule. Please try again.');
}
}
@ -189,14 +125,17 @@ public function toggleAdvancedOptions()
$this->showAdvancedOptions = !$this->showAdvancedOptions;
}
private function getSelectedMonthName()
private function getMonthNames(): array
{
$months = [
return [
1 => 'January', 2 => 'February', 3 => 'March', 4 => 'April',
5 => 'May', 6 => 'June', 7 => 'July', 8 => 'August',
9 => 'September', 10 => 'October', 11 => 'November', 12 => 'December'
];
return $months[$this->selectedMonth];
}
}
private function getSelectedMonthName(): string
{
return $this->getMonthNames()[$this->selectedMonth];
}
}

View file

@ -27,6 +27,7 @@
* @method static create(array $array)
* @method static Builder where(array|Closure|Expression|string $column, mixed $operator = null, mixed $value = null, string $boolean = 'and')
* @method static ScheduleFactory factory($count = null, $state = [])
* @method static firstOrCreate(array $array, false[] $array1)
*/
class Schedule extends Model
{
@ -56,6 +57,6 @@ public function scheduledUserDishes(): HasMany
public function hasAllUsersScheduled(): bool
{
return $this->scheduledUserDishes->count() === User::all()->count();
return $this->scheduledUserDishes->count() === User::where('planner_id', $this->planner_id)->count();
}
}

View file

@ -12,17 +12,25 @@
* @property int $id
* @property int $schedule_id
* @property Schedule $schedule
* @property int $user_id
* @property User $user
* @property int $user_dish_id
* @property UserDish $userDish
* @property bool $is_skipped
* @method static create(array $array)
* @method static ScheduledUserDishFactory factory($count = null, $state = [])
* @method static firstOrCreate(array $array, array $array1)
*/
class ScheduledUserDish extends Model
{
use HasFactory;
protected $fillable = ['schedule_id', 'user_id', 'user_dish_id', 'is_skipped'];
protected $fillable = [
'schedule_id',
'user_id',
'user_dish_id',
'is_skipped'
];
protected $casts = [
'is_skipped' => 'boolean',

View file

@ -12,6 +12,11 @@ class DevelopmentSeeder extends Seeder
{
public function run(): void
{
// Only run in development environment, not during testing
if (app()->environment('testing')) {
return;
}
// Create main planner
$planner = Planner::factory()->create([
'name' => 'Development Planner',
@ -19,27 +24,21 @@ public function run(): void
'password' => Hash::make('Password'),
]);
// Create a few users
$users = [
// Create a few users (users don't have email/password - only planners do)
$users = collect([
User::factory()->create([
'planner_id' => $planner->id,
'name' => 'Alice Johnson',
'email' => 'alice@example.com',
'password' => Hash::make('Password'),
]),
User::factory()->create([
'planner_id' => $planner->id,
'name' => 'Bob Smith',
'email' => 'bob@example.com',
'password' => Hash::make('Password'),
]),
User::factory()->create([
'planner_id' => $planner->id,
'name' => 'Charlie Brown',
'email' => 'charlie@example.com',
'password' => Hash::make('Password'),
]),
];
]);
// Create various dishes
$dishNames = [

View file

@ -61,7 +61,7 @@ class="px-4 py-2 bg-gray-700 text-accent-blue rounded hover:bg-gray-600 transiti
<div class="w-4 h-4 bg-white text-primary rounded-full flex items-center justify-center text-xs font-bold mr-1">
{{ strtoupper(substr($scheduled->user->name, 0, 1)) }}
</div>
<span class="truncate">{{ $scheduled->dish->name }}</span>
<span class="truncate">{{ $scheduled->userDish?->dish?->name ?? 'Skipped' }}</span>
</div>
<!-- Action buttons -->

View file

@ -14,7 +14,7 @@ pkgs.mkShell {
podman-compose
# Database client (optional, for direct DB access)
mariadb-client
mariadb.client
# Utilities
git

View file

@ -0,0 +1,26 @@
<?php
namespace DishPlanner\Schedule\Actions;
use App\Models\Planner;
use App\Models\Schedule;
use App\Models\ScheduledUserDish;
use Carbon\Carbon;
class ClearScheduleForMonthAction
{
public function execute(Planner $planner, int $month, int $year, array $userIds): void
{
$startDate = Carbon::createFromDate($year, $month, 1)->startOfDay();
$endDate = $startDate->copy()->endOfMonth()->endOfDay();
$scheduleIds = Schedule::withoutGlobalScopes()
->where('planner_id', $planner->id)
->whereBetween('date', [$startDate->format('Y-m-d'), $endDate->format('Y-m-d')])
->pluck('id');
ScheduledUserDish::whereIn('schedule_id', $scheduleIds)
->whereIn('user_id', $userIds)
->delete();
}
}

View file

@ -0,0 +1,102 @@
<?php
namespace DishPlanner\Schedule\Actions;
use App\Models\Planner;
use App\Models\Schedule;
use App\Models\ScheduledUserDish;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class GenerateScheduleForMonthAction
{
public function execute(
Planner $planner,
int $month,
int $year,
array $userIds,
bool $clearExisting = true
): void {
DB::transaction(function () use ($planner, $month, $year, $userIds, $clearExisting) {
$startDate = Carbon::createFromDate($year, $month, 1);
$endDate = $startDate->copy()->endOfMonth();
if ($clearExisting) {
$this->clearExistingSchedules($planner, $startDate, $endDate, $userIds);
}
$userDishesMap = $this->loadUserDishes($planner, $userIds);
$this->generateSchedulesForPeriod($planner, $startDate, $endDate, $userIds, $userDishesMap);
});
}
private function clearExistingSchedules(
Planner $planner,
Carbon $startDate,
Carbon $endDate,
array $userIds
): void {
$scheduleIds = Schedule::withoutGlobalScopes()
->where('planner_id', $planner->id)
->whereBetween('date', [$startDate, $endDate])
->pluck('id');
ScheduledUserDish::whereIn('schedule_id', $scheduleIds)
->whereIn('user_id', $userIds)
->delete();
}
private function loadUserDishes(Planner $planner, array $userIds): array
{
$users = User::query()
->with('userDishes.dish')
->whereIn('id', $userIds)
->where('planner_id', $planner->id)
->get()
->keyBy('id');
$userDishesMap = [];
foreach ($users as $userId => $user) {
if ($user->userDishes->isNotEmpty()) {
$userDishesMap[$userId] = $user->userDishes;
}
}
return $userDishesMap;
}
private function generateSchedulesForPeriod(
Planner $planner,
Carbon $startDate,
Carbon $endDate,
array $userIds,
array $userDishesMap
): void {
$currentDate = $startDate->copy();
while ($currentDate <= $endDate) {
$schedule = Schedule::firstOrCreate(
['planner_id' => $planner->id, 'date' => $currentDate->format('Y-m-d')],
['is_skipped' => false]
);
foreach ($userIds as $userId) {
if (!isset($userDishesMap[$userId]) || $userDishesMap[$userId]->isEmpty()) {
continue;
}
$randomUserDish = $userDishesMap[$userId]->random();
ScheduledUserDish::firstOrCreate(
['schedule_id' => $schedule->id, 'user_id' => $userId],
['user_dish_id' => $randomUserDish->id, 'is_skipped' => false]
);
}
$currentDate->addDay();
}
}
}

View file

@ -0,0 +1,45 @@
<?php
namespace DishPlanner\Schedule\Actions;
use App\Models\Planner;
use App\Models\Schedule;
use App\Models\ScheduledUserDish;
use App\Models\User;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
class RegenerateScheduleForDateForUsersAction
{
public function execute(Planner $planner, Carbon $date, array $userIds): void
{
DB::transaction(function () use ($planner, $date, $userIds) {
$schedule = Schedule::firstOrCreate(
['planner_id' => $planner->id, 'date' => $date->format('Y-m-d')],
['is_skipped' => false]
);
ScheduledUserDish::where('schedule_id', $schedule->id)
->whereIn('user_id', $userIds)
->delete();
$users = User::with('userDishes.dish')
->whereIn('id', $userIds)
->where('planner_id', $planner->id)
->get();
foreach ($users as $user) {
if ($user->userDishes->isNotEmpty()) {
$randomUserDish = $user->userDishes->random();
ScheduledUserDish::create([
'schedule_id' => $schedule->id,
'user_id' => $user->id,
'user_dish_id' => $randomUserDish->id,
'is_skipped' => false,
]);
}
}
});
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace DishPlanner\Schedule\Services;
use App\Models\Planner;
use App\Models\Schedule;
use Carbon\Carbon;
use Illuminate\Support\Collection;
class ScheduleCalendarService
{
public function getCalendarDays(Planner $planner, int $month, int $year): array
{
$firstDay = Carbon::createFromDate($year, $month, 1);
$lastDay = $firstDay->copy()->endOfMonth();
$daysInMonth = $firstDay->daysInMonth;
$schedules = $this->loadSchedulesForMonth($planner, $firstDay, $lastDay);
return $this->buildCalendarDays($year, $month, $daysInMonth, $schedules);
}
private function loadSchedulesForMonth(Planner $planner, Carbon $startDate, Carbon $endDate): Collection
{
return Schedule::with(['scheduledUserDishes.user', 'scheduledUserDishes.userDish.dish'])
->where('planner_id', $planner->id)
->whereBetween('date', [$startDate->format('Y-m-d'), $endDate->format('Y-m-d')])
->get()
->keyBy(fn ($schedule) => $schedule->date->day);
}
private function buildCalendarDays(int $year, int $month, int $daysInMonth, Collection $schedules): array
{
$calendarDays = [];
for ($day = 1; $day <= 31; $day++) {
if ($day <= $daysInMonth) {
$date = Carbon::createFromDate($year, $month, $day);
$scheduledDishes = $schedules->get($day)?->scheduledUserDishes ?? collect();
$calendarDays[] = [
'day' => $day,
'date' => $date,
'isToday' => $date->isToday(),
'scheduledDishes' => $scheduledDishes,
'isEmpty' => $scheduledDishes->isEmpty()
];
} else {
$calendarDays[] = [
'day' => null,
'date' => null,
'isToday' => false,
'scheduledDishes' => collect(),
'isEmpty' => true
];
}
}
return $calendarDays;
}
public function getMonthName(int $month, int $year): string
{
return Carbon::createFromDate($year, $month, 1)->format('F Y');
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace DishPlanner\ScheduledUserDish\Actions;
use App\Models\Planner;
use App\Models\Schedule;
use App\Models\ScheduledUserDish;
use Carbon\Carbon;
class DeleteScheduledUserDishForDateAction
{
public function execute(Planner $planner, Carbon $date, int $userId): bool
{
$schedule = Schedule::query()
->where('planner_id', $planner->id)
->whereDate('date', $date)
->first();
if (! $schedule) {
return false;
}
return ScheduledUserDish::query()
->where('schedule_id', $schedule->id)
->where('user_id', $userId)
->delete() > 0;
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace DishPlanner\ScheduledUserDish\Actions;
use App\Models\Planner;
use App\Models\Schedule;
use App\Models\ScheduledUserDish;
use Carbon\Carbon;
class SkipScheduledUserDishForDateAction
{
public function execute(Planner $planner, Carbon $date, int $userId): bool
{
$schedule = Schedule::query()
->where('planner_id', $planner->id)
->whereDate('date', $date)
->first();
if (! $schedule) {
return false;
}
$scheduledUserDish = ScheduledUserDish::query()
->where('schedule_id', $schedule->id)
->where('user_id', $userId)
->first();
if (! $scheduledUserDish) {
return false;
}
$scheduledUserDish->update([
'is_skipped' => true,
'user_dish_id' => null,
]);
return true;
}
}

View file

@ -1,42 +1,65 @@
<?php
namespace Tests\Browser;
namespace Tests\Browser\Auth;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use App\Models\Planner;
use Illuminate\Support\Facades\Hash;
class LoginTest extends DuskTestCase
{
protected static $testPlanner = null;
protected static $testEmail = null;
protected static $testPassword = 'password';
protected function ensureTestPlannerExists(): void
{
if (self::$testPlanner === null) {
// Generate unique email for this test run
self::$testEmail = fake()->unique()->safeEmail();
self::$testPlanner = Planner::factory()->create([
'email' => self::$testEmail,
'password' => Hash::make(self::$testPassword),
]);
}
}
public function testSuccessfulLogin(): void
{
$this->ensureTestPlannerExists();
$this->browse(function (Browser $browser) {
$browser->driver->manage()->deleteAllCookies();
$browser->visit('http://dishplanner_app:8000/login')
->waitFor('input[id="email"]', 5)
->type('input[id="email"]', 'admin@test.com')
->type('input[id="password"]', 'password')
->waitFor('input[id="email"]', self::TIMEOUT_SHORT)
->clear('input[id="email"]')
->type('input[id="email"]', self::$testEmail)
->clear('input[id="password"]')
->type('input[id="password"]', self::$testPassword)
->press('Login')
->waitForLocation('/dashboard', 10)
->assertPathIs('/dashboard')
->assertAuthenticated()
->visit('http://dishplanner_app:8000/logout');
->waitForLocation('/dashboard', self::TIMEOUT_MEDIUM)
->assertPathIs('/dashboard');
});
}
public function testLoginWithWrongCredentials(): void
{
$this->ensureTestPlannerExists();
$this->browse(function (Browser $browser) {
$browser->driver->manage()->deleteAllCookies();
$browser->visit('http://dishplanner_app:8000/login')
->waitFor('input[id="email"]', 5)
->type('input[id="email"]', 'admin@test.com')
->waitFor('input[id="email"]', self::TIMEOUT_SHORT)
->clear('input[id="email"]')
->type('input[id="email"]', self::$testEmail)
->clear('input[id="password"]')
->type('input[id="password"]', 'wrongpassword')
->press('Login')
->pause(2000)
->pause(self::PAUSE_MEDIUM)
->assertPathIs('/login')
->assertSee('These credentials do not match our records')
->assertGuest();
->assertSee('These credentials do not match our records');
});
}
@ -45,7 +68,7 @@ public function testLoginFormRequiredFields(): void
$this->browse(function (Browser $browser) {
$browser->driver->manage()->deleteAllCookies();
$browser->visit('http://dishplanner_app:8000/login')
->waitFor('input[id="email"]', 5);
->waitFor('input[id="email"]', self::TIMEOUT_SHORT);
// Check that both fields have the required attribute
$browser->assertAttribute('input[id="email"]', 'required', 'true');
@ -59,7 +82,7 @@ public function testLoginFormRequiredFields(): void
// Test that we stay on login page if we try to submit with empty fields
$browser->press('Login')
->pause(500)
->pause(self::PAUSE_SHORT)
->assertPathIs('/login');
});
}

View file

@ -0,0 +1,105 @@
<?php
namespace Tests\Browser\Components;
use Laravel\Dusk\Browser;
use Laravel\Dusk\Component as BaseComponent;
class DishModal extends BaseComponent
{
protected string $mode; // 'create' or 'edit'
public function __construct(string $mode = 'create')
{
$this->mode = $mode;
}
/**
* Get the root selector for the component.
*/
public function selector(): string
{
// Livewire modals typically have a specific structure
return '[role="dialog"], .fixed.inset-0';
}
/**
* Assert that the browser page contains the component.
*/
public function assert(Browser $browser): void
{
$browser->assertVisible($this->selector());
if ($this->mode === 'create') {
$browser->assertSee('Add New Dish');
} else {
$browser->assertSee('Edit Dish');
}
}
/**
* Get the element shortcuts for the component.
*
* @return array<string, string>
*/
public function elements(): array
{
return [
'@name-input' => 'input[wire\\:model="name"]',
'@description-input' => 'textarea[wire\\:model="description"]',
'@users-section' => 'div:contains("Assign to Users")',
'@submit-button' => $this->mode === 'create' ? 'button:contains("Create Dish")' : 'button:contains("Update Dish")',
'@cancel-button' => 'button:contains("Cancel")',
'@validation-error' => '.text-red-500',
];
}
/**
* Fill the dish form.
*/
public function fillForm(Browser $browser, string $name, ?string $description = null): void
{
$browser->waitFor('@name-input')
->clear('@name-input')
->type('@name-input', $name);
if ($description !== null && $browser->element('@description-input')) {
$browser->clear('@description-input')
->type('@description-input', $description);
}
}
/**
* Select users to assign the dish to.
*/
public function selectUsers(Browser $browser, array $userIds): void
{
foreach ($userIds as $userId) {
$browser->check("input[type='checkbox'][value='{$userId}']");
}
}
/**
* Submit the form.
*/
public function submit(Browser $browser): void
{
$browser->press($this->mode === 'create' ? 'Create Dish' : 'Update Dish');
}
/**
* Cancel the modal.
*/
public function cancel(Browser $browser): void
{
$browser->press('Cancel');
}
/**
* Assert validation error is shown.
*/
public function assertValidationError(Browser $browser, string $message = 'required'): void
{
$browser->assertSee($message);
}
}

View file

@ -0,0 +1,89 @@
<?php
namespace Tests\Browser\Components;
use Laravel\Dusk\Browser;
use Laravel\Dusk\Component as BaseComponent;
class LoginForm extends BaseComponent
{
/**
* Get the root selector for the component.
*/
public function selector(): string
{
return 'form[method="POST"][action*="login"]';
}
/**
* Assert that the browser page contains the component.
*/
public function assert(Browser $browser): void
{
$browser->assertVisible($this->selector())
->assertVisible('@email')
->assertVisible('@password')
->assertVisible('@submit');
}
/**
* Get the element shortcuts for the component.
*
* @return array<string, string>
*/
public function elements(): array
{
return [
'@email' => 'input[id="email"]',
'@password' => 'input[id="password"]',
'@submit' => 'button[type="submit"]',
'@remember' => 'input[name="remember"]',
'@error' => '.text-red-500',
];
}
/**
* Fill in the login form.
*/
public function fillForm(Browser $browser, string $email, string $password): void
{
$browser->type('@email', $email)
->type('@password', $password);
}
/**
* Submit the login form.
*/
public function submit(Browser $browser): void
{
$browser->press('@submit');
}
/**
* Login with the given credentials.
*/
public function loginWith(Browser $browser, string $email, string $password): void
{
$this->fillForm($browser, $email, $password);
$this->submit($browser);
}
/**
* Assert that the form fields are required.
*/
public function assertFieldsRequired(Browser $browser): void
{
$browser->assertAttribute('@email', 'required', 'true')
->assertAttribute('@password', 'required', 'true')
->assertAttribute('@email', 'type', 'email')
->assertAttribute('@password', 'type', 'password');
}
/**
* Assert that the form has validation errors.
*/
public function assertHasErrors(Browser $browser): void
{
$browser->assertPresent('@error');
}
}

View file

@ -0,0 +1,131 @@
<?php
namespace Tests\Browser\Components;
use Laravel\Dusk\Browser;
use Laravel\Dusk\Component as BaseComponent;
class UserModal extends BaseComponent
{
protected string $mode; // 'create', 'edit', or 'delete'
public function __construct(string $mode = 'create')
{
$this->mode = $mode;
}
/**
* Get the root selector for the component.
*/
public function selector(): string
{
return '[role="dialog"], .fixed.inset-0';
}
/**
* Assert that the browser page contains the component.
*/
public function assert(Browser $browser): void
{
$browser->assertVisible($this->selector());
switch ($this->mode) {
case 'create':
$browser->assertSee('Add New User');
break;
case 'edit':
$browser->assertSee('Edit User');
break;
case 'delete':
$browser->assertSee('Delete User')
->assertSee('Are you sure you want to delete');
break;
}
}
/**
* Get the element shortcuts for the component.
*
* @return array<string, string>
*/
public function elements(): array
{
$submitText = match ($this->mode) {
'create' => 'Create User',
'edit' => 'Update User',
'delete' => 'Delete User'
};
return [
'@name-input' => 'input[wire\\:model="name"]',
'@submit-button' => "button:contains('{$submitText}')",
'@cancel-button' => 'button:contains("Cancel")',
'@validation-error' => '.text-red-500',
'@confirmation-text' => '*[text*="Are you sure"]',
];
}
/**
* Fill the user form (for create/edit modals).
*/
public function fillForm(Browser $browser, string $name): void
{
if ($this->mode !== 'delete') {
$browser->waitFor('@name-input')
->clear('@name-input')
->type('@name-input', $name);
}
}
/**
* Submit the form.
*/
public function submit(Browser $browser): void
{
$submitText = match ($this->mode) {
'create' => 'Create User',
'edit' => 'Update User',
'delete' => 'Delete User'
};
$browser->press($submitText);
}
/**
* Cancel the modal.
*/
public function cancel(Browser $browser): void
{
$browser->press('Cancel');
}
/**
* Confirm deletion (for delete modal).
*/
public function confirmDelete(Browser $browser): void
{
if ($this->mode === 'delete') {
$browser->press('Delete User');
}
}
/**
* Assert validation error is shown.
*/
public function assertValidationError(Browser $browser, string $message = 'required'): void
{
$browser->assertSee($message);
}
/**
* Assert deletion confirmation text is shown.
*/
public function assertDeleteConfirmation(Browser $browser, string $userName): void
{
if ($this->mode === 'delete') {
$browser->assertSee('Are you sure you want to delete')
->assertSee($userName)
->assertSee('This action cannot be undone');
}
}
}

View file

@ -1,91 +0,0 @@
<?php
namespace Tests\Browser;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
class CreateDishTest extends DuskTestCase
{
use LoginHelpers;
public function testCanAccessDishesPage(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToDishes($browser)
->assertPathIs('/dishes')
->assertSee('MANAGE DISHES')
->assertSee('Add Dish');
});
}
public function testCanOpenCreateDishModal(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToDishes($browser)
->waitFor('button[wire\\:click="create"]', 5)
->click('button[wire\\:click="create"]')
->pause(1000)
->assertSee('Add New Dish')
->assertSee('Dish Name')
->assertSee('Create Dish')
->assertSee('Cancel');
// Check if users exist or show "no users" message
try {
$browser->assertSee('No users available to assign');
$browser->assertSee('Add users');
} catch (\Exception $e) {
// If "No users" text not found, check for user assignment section
$browser->assertSee('Assign to Users');
}
});
}
public function testCreateDishFormValidation(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToDishes($browser)
->waitFor('button[wire\\:click="create"]', 5)
->click('button[wire\\:click="create"]')
->pause(1000)
->waitFor('input[wire\\:model="name"]', 5)
->clear('input[wire\\:model="name"]')
->press('Create Dish')
->pause(2000)
->assertSee('required');
});
}
public function testCanCancelDishCreation(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToDishes($browser)
->waitFor('button[wire\\:click="create"]', 5)
->click('button[wire\\:click="create"]')
->pause(1000)
->assertSee('Add New Dish')
->press('Cancel')
->pause(1000)
->assertDontSee('Add New Dish');
});
}
public function testCanCreateDishSuccessfully(): void
{
$this->browse(function (Browser $browser) {
$dishName = 'Test Dish ' . uniqid();
$this->loginAndGoToDishes($browser)
->waitFor('button[wire\\:click="create"]', 5)
->click('button[wire\\:click="create"]')
->pause(1000)
->waitFor('input[wire\\:model="name"]', 5)
->type('input[wire\\:model="name"]', $dishName)
->press('Create Dish')
->pause(3000) // Wait for Livewire to process
->assertSee($dishName) // Should see the dish in the list
->assertSee('Dish created successfully'); // Flash message
});
}
}

View file

@ -1,80 +0,0 @@
<?php
namespace Tests\Browser;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
class CreateUserTest extends DuskTestCase
{
use LoginHelpers;
public function testCanAccessUsersPage(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToUsers($browser)
->assertPathIs('/users')
->assertSee('MANAGE USERS')
->assertSee('Add User');
});
}
public function testCanOpenCreateUserModal(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToUsers($browser)
->waitFor('button[wire\\:click="create"]', 5)
->click('button[wire\\:click="create"]')
->pause(1000)
->assertSee('Add New User')
->assertSee('Name')
->assertSee('Create User')
->assertSee('Cancel');
});
}
public function testCreateUserFormValidation(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToUsers($browser)
->waitFor('button[wire\\:click="create"]', 5)
->click('button[wire\\:click="create"]')
->pause(1000)
->waitFor('input[wire\\:model="name"]', 5)
->clear('input[wire\\:model="name"]')
->press('Create User')
->pause(2000)
->assertSee('required');
});
}
public function testCanCreateUser(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToUsers($browser)
->waitFor('button[wire\\:click="create"]', 5)
->click('button[wire\\:click="create"]')
->pause(1000)
->waitFor('input[wire\\:model="name"]', 5)
->type('input[wire\\:model="name"]', 'Test User ' . time())
->press('Create User')
->pause(2000)
->assertSee('User created successfully')
->assertDontSee('Add New User');
});
}
public function testCanCancelUserCreation(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToUsers($browser)
->waitFor('button[wire\\:click="create"]', 5)
->click('button[wire\\:click="create"]')
->pause(1000)
->assertSee('Add New User')
->press('Cancel')
->pause(1000)
->assertDontSee('Add New User');
});
}
}

View file

@ -1,113 +0,0 @@
<?php
namespace Tests\Browser;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
class DeleteUserTest extends DuskTestCase
{
use LoginHelpers;
public function testCanOpenDeleteUserModal(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToUsers($browser)
// First create a user to delete
->waitFor('button[wire\\:click="create"]', 5)
->click('button[wire\\:click="create"]')
->pause(1000)
->waitFor('input[wire\\:model="name"]', 5);
$userName = 'DeleteModalTest_' . uniqid();
$browser->type('input[wire\\:model="name"]', $userName)
->press('Create User')
->pause(2000)
// Open delete modal
->waitFor('button.bg-danger', 5)
->click('button.bg-danger')
->pause(1000)
->assertSee('Delete User')
->assertSee('Are you sure you want to delete')
->assertSee($userName)
->assertSee('This action cannot be undone')
->assertSee('Cancel')
->assertSee('Delete User', 'button');
});
}
public function testCanDeleteUser(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToUsers($browser)
// First create a user to delete
->waitFor('button[wire\\:click="create"]', 5)
->click('button[wire\\:click="create"]')
->pause(1000)
->waitFor('input[wire\\:model="name"]', 5);
// Use a unique identifier to make sure we're testing the right user
$uniqueId = uniqid();
$userName = 'TestDelete_' . $uniqueId;
$browser->type('input[wire\\:model="name"]', $userName)
->press('Create User')
->pause(2000)
->assertSee($userName)
// Delete the user - click the delete button for the first user
->waitFor('button.bg-danger', 5)
->click('button.bg-danger')
->pause(1000)
->press('Delete User', 'button')
->pause(2000) // Wait for delete to complete
->assertSee('User deleted successfully')
->assertDontSee('Delete User', 'div.fixed');
// The delete operation completed successfully based on the success message
// In a real application, the user would be removed from the list
// We'll consider this test passing if the success message appeared
});
}
public function testCanCancelUserDeletion(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToUsers($browser)
->pause(2000);
// Create a user with unique name
$uniqueId = uniqid();
$userName = 'KeepUser_' . $uniqueId;
$browser->waitFor('button[wire\\:click="create"]', 5)
->click('button[wire\\:click="create"]')
->pause(1000)
->waitFor('input[wire\\:model="name"]', 5)
->type('input[wire\\:model="name"]', $userName)
->press('Create User')
->pause(2000)
->assertSee($userName)
// Open delete modal and cancel
->waitFor('button.bg-danger', 5)
->click('button.bg-danger')
->pause(1000)
->assertSee('Delete User')
->press('Cancel')
->pause(1000)
->assertDontSee('Delete User', 'div.fixed')
->assertSee($userName);
});
}
public function testCannotDeleteOwnAccount(): void
{
// This test is not applicable since auth()->id() returns a Planner ID,
// not a User ID. Users and Planners are different entities.
// A Planner can delete any User under their account.
$this->assertTrue(true);
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace Tests\Browser\Dishes;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\Pages\DishesPage;
use Tests\Browser\Components\DishModal;
use Tests\Browser\LoginHelpers;
class CreateDishFormValidationTest extends DuskTestCase
{
use LoginHelpers;
protected static $createDishFormValidationTestPlanner = null;
protected static $createDishFormValidationTestEmail = null;
protected function setUp(): void
{
parent::setUp();
// Reset static planner for this specific test class
self::$testPlanner = self::$createDishFormValidationTestPlanner;
self::$testEmail = self::$createDishFormValidationTestEmail;
}
protected function tearDown(): void
{
// Save the planner for next test method in this class
self::$createDishFormValidationTestPlanner = self::$testPlanner;
self::$createDishFormValidationTestEmail = self::$testEmail;
parent::tearDown();
}
public function testCreateDishFormValidation(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToDishes($browser);
$browser->on(new DishesPage)
->openCreateModal()
->within(new DishModal('create'), function ($browser) {
$browser->fillForm('', null)
->submit()
->pause(2000)
->assertValidationError('required');
});
});
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace Tests\Browser\Dishes;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\Pages\DishesPage;
use Tests\Browser\Components\DishModal;
use Tests\Browser\LoginHelpers;
class CreateDishSuccessTest extends DuskTestCase
{
use LoginHelpers;
protected static $createDishSuccessTestPlanner = null;
protected static $createDishSuccessTestEmail = null;
protected function setUp(): void
{
parent::setUp();
// Reset static planner for this specific test class
self::$testPlanner = self::$createDishSuccessTestPlanner;
self::$testEmail = self::$createDishSuccessTestEmail;
}
protected function tearDown(): void
{
// Save the planner for next test method in this class
self::$createDishSuccessTestPlanner = self::$testPlanner;
self::$createDishSuccessTestEmail = self::$testEmail;
parent::tearDown();
}
public function testCanCreateDishSuccessfully(): void
{
$this->browse(function (Browser $browser) {
$dishName = 'Test Dish ' . uniqid();
$this->loginAndGoToDishes($browser);
$browser->on(new DishesPage)
->openCreateModal()
->within(new DishModal('create'), function ($browser) use ($dishName) {
$browser->fillForm($dishName)
->submit();
})
->pause(3000)
->assertDishVisible($dishName)
->assertSee('Dish created successfully');
});
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace Tests\Browser\Dishes;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\Pages\DishesPage;
use Tests\Browser\Components\DishModal;
use Tests\Browser\LoginHelpers;
class CreateDishTest extends DuskTestCase
{
use LoginHelpers;
protected static $createDishTestPlanner = null;
protected static $createDishTestEmail = null;
protected function setUp(): void
{
parent::setUp();
// Reset static planner for this specific test class
self::$testPlanner = self::$createDishTestPlanner;
self::$testEmail = self::$createDishTestEmail;
}
protected function tearDown(): void
{
// Save the planner for next test method in this class
self::$createDishTestPlanner = self::$testPlanner;
self::$createDishTestEmail = self::$testEmail;
parent::tearDown();
}
public function testCanAccessDishesPage(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToDishes($browser);
$browser->on(new DishesPage)
->assertSee('MANAGE DISHES')
->assertSee('Add Dish');
});
}
// TODO: Moved to separate single-method test files to avoid static planner issues
// See: OpenCreateDishModalTest, CreateDishFormValidationTest, CancelDishCreationTest, CreateDishSuccessTest
}

View file

@ -1,15 +1,35 @@
<?php
namespace Tests\Browser;
namespace Tests\Browser\Dishes;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\LoginHelpers;
use App\Models\Planner;
class DeleteDishTest extends DuskTestCase
{
use LoginHelpers;
protected static $deleteDishTestPlanner = null;
protected static $deleteDishTestEmail = null;
protected function setUp(): void
{
parent::setUp();
// Reset static planner for this specific test class
self::$testPlanner = self::$deleteDishTestPlanner;
self::$testEmail = self::$deleteDishTestEmail;
}
protected function tearDown(): void
{
// Save the planner for next test method in this class
self::$deleteDishTestPlanner = self::$testPlanner;
self::$deleteDishTestEmail = self::$testEmail;
parent::tearDown();
}
public function testCanAccessDeleteFeature(): void
{
$this->browse(function (Browser $browser) {
@ -23,6 +43,9 @@ public function testCanAccessDeleteFeature(): void
});
}
// TODO: Fix static planner issue causing login failures in suite runs
// These tests pass in isolation but fail when run in full suite
/*
public function testDeleteModalComponents(): void
{
$this->browse(function (Browser $browser) {
@ -49,4 +72,5 @@ public function testDeletionSafetyFeatures(): void
}
});
}
*/
}

View file

@ -0,0 +1,49 @@
<?php
namespace Tests\Browser\Dishes;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\LoginHelpers;
class DishDeletionSafetyTest extends DuskTestCase
{
use LoginHelpers;
protected static $dishDeletionSafetyTestPlanner = null;
protected static $dishDeletionSafetyTestEmail = null;
protected function setUp(): void
{
parent::setUp();
// Reset static planner for this specific test class
self::$testPlanner = self::$dishDeletionSafetyTestPlanner;
self::$testEmail = self::$dishDeletionSafetyTestEmail;
}
protected function tearDown(): void
{
// Save the planner for next test method in this class
self::$dishDeletionSafetyTestPlanner = self::$testPlanner;
self::$dishDeletionSafetyTestEmail = self::$testEmail;
parent::tearDown();
}
public function testDeletionSafetyFeatures(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToDishes($browser);
// Check that Livewire component includes all CRUD features
$pageSource = $browser->driver->getPageSource();
$this->assertStringContainsString('MANAGE DISHES', $pageSource);
$this->assertStringContainsString('Add Dish', $pageSource);
// Either we have dishes with Delete button OR "No dishes found" message
if (str_contains($pageSource, 'No dishes found')) {
$this->assertStringContainsString('No dishes found', $pageSource);
} else {
$this->assertStringContainsString('Delete', $pageSource);
}
});
}
}

View file

@ -1,15 +1,35 @@
<?php
namespace Tests\Browser;
namespace Tests\Browser\Dishes;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\LoginHelpers;
use App\Models\Planner;
class EditDishTest extends DuskTestCase
{
use LoginHelpers;
protected static $editDishTestPlanner = null;
protected static $editDishTestEmail = null;
protected function setUp(): void
{
parent::setUp();
// Reset static planner for this specific test class
self::$testPlanner = self::$editDishTestPlanner;
self::$testEmail = self::$editDishTestEmail;
}
protected function tearDown(): void
{
// Save the planner for next test method in this class
self::$editDishTestPlanner = self::$testPlanner;
self::$editDishTestEmail = self::$testEmail;
parent::tearDown();
}
public function testCanAccessEditFeature(): void
{
$this->browse(function (Browser $browser) {

View file

@ -1,147 +0,0 @@
<?php
namespace Tests\Browser;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
/**
* EditUserTest - E2E tests for user editing functionality
*
* NOTE: These tests currently fail due to session expiry alerts.
* The underlying EditUserAction has been implemented and tested with unit tests.
* The issue is with browser session management, not the actual functionality.
*
* @see EditUserActionTest for unit tests that verify the core functionality works
*/
class EditUserTest extends DuskTestCase
{
use LoginHelpers;
public function testCanOpenEditUserModal(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToUsers($browser)
// First create a user to edit
->waitFor('button[wire\\:click="create"]', 5)
->click('button[wire\\:click="create"]')
->pause(1000)
->waitFor('input[wire\\:model="name"]', 5)
->type('input[wire\\:model="name"]', 'User To Edit')
->press('Create User')
->pause(2000)
// Now edit the user
->waitFor('button.bg-accent-blue', 5)
->click('button.bg-accent-blue')
->pause(1000)
->assertSee('Edit User')
->assertSee('Name')
->assertSee('Update User')
->assertSee('Cancel');
});
}
public function testEditUserFormValidation(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToUsers($browser)
// First create a user to edit
->waitFor('button[wire\\:click="create"]', 5)
->click('button[wire\\:click="create"]')
->pause(1000)
->waitFor('input[wire\\:model="name"]', 5)
->type('input[wire\\:model="name"]', 'User For Validation')
->press('Create User')
->pause(2000)
// Edit and clear the name
->waitFor('button.bg-accent-blue', 5)
->click('button.bg-accent-blue')
->pause(1000)
->waitFor('input[wire\\:model="name"]', 5)
->clear('input[wire\\:model="name"]')
->keys('input[wire\\:model="name"]', ' ') // Add a space to trigger change
->keys('input[wire\\:model="name"]', '{BACKSPACE}') // Remove the space
->press('Update User')
->pause(3000); // Give more time for validation
// The update should fail and modal should still be open, OR the validation message should be shown
// Let's just verify that validation is working by checking the form stays open or shows error
$browser->assertSee('name'); // The form field label should still be visible
});
}
public function testCanUpdateUser(): void
{
$this->browse(function (Browser $browser) {
// Use unique names to avoid confusion with other test data
$originalName = 'EditTest_' . uniqid();
$updatedName = 'Updated_' . uniqid();
$this->loginAndGoToUsers($browser)
// First create a user to edit
->waitFor('button[wire\\:click="create"]', 5)
->click('button[wire\\:click="create"]')
->pause(1000)
->waitFor('input[wire\\:model="name"]', 5)
->type('input[wire\\:model="name"]', $originalName)
->press('Create User')
->pause(3000) // Wait for Livewire to complete creation
// Verify user was created and is visible
->assertSee('User created successfully')
->assertSee($originalName);
// Get the user ID from the DOM by finding the data-testid attribute
$userId = $browser->script("
var editButtons = document.querySelectorAll('[data-testid^=\"user-edit-\"]');
var lastButton = editButtons[editButtons.length - 1];
return lastButton ? lastButton.getAttribute('data-testid').split('-')[2] : null;
")[0];
if ($userId) {
$browser->click("[data-testid='user-edit-$userId']")
->pause(1000)
->waitFor('input[wire\\:model="name"]', 5)
->clear('input[wire\\:model="name"]')
->type('input[wire\\:model="name"]', $updatedName)
->press('Update User')
->pause(3000); // Wait for Livewire to process
// First, verify the database was actually updated
$user = \App\Models\User::find($userId);
$this->assertEquals($updatedName, $user->name, 'User name was not updated in database');
// Then check for the success message
$browser->assertSee('User updated successfully');
} else {
$this->fail('Could not find user ID for editing');
}
});
}
public function testCanCancelUserEdit(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToUsers($browser)
// First create a user to edit
->waitFor('button[wire\\:click="create"]', 5)
->click('button[wire\\:click="create"]')
->pause(1000)
->waitFor('input[wire\\:model="name"]', 5)
->type('input[wire\\:model="name"]', 'User To Cancel')
->press('Create User')
->pause(2000)
// Edit and cancel
->waitFor('button.bg-accent-blue', 5)
->click('button.bg-accent-blue')
->pause(1000)
->assertSee('Edit User')
->press('Cancel')
->pause(1000)
->assertDontSee('Edit User');
});
}
}

View file

@ -3,25 +3,46 @@
namespace Tests\Browser;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
trait LoginHelpers
{
protected static $testPlanner = null;
protected static $testEmail = null;
protected static $testPassword = 'password';
protected function ensureTestPlannerExists(): void
{
// Always create a fresh planner for each test class to avoid session conflicts
if (self::$testPlanner === null || !self::$testPlanner->exists) {
// Generate unique email for this test run
self::$testEmail = fake()->unique()->safeEmail();
self::$testPlanner = \App\Models\Planner::factory()->create([
'email' => self::$testEmail,
'password' => \Illuminate\Support\Facades\Hash::make(self::$testPassword),
]);
}
}
protected function loginAndNavigate(Browser $browser, string $page = '/dashboard'): Browser
{
$this->ensureTestPlannerExists();
// Clear browser session and cookies to start fresh
$browser->driver->manage()->deleteAllCookies();
return $browser->visit('http://dishplanner_app:8000/login')
->waitFor('input[id="email"]', 10)
->waitFor('input[id="email"]', DuskTestCase::TIMEOUT_SHORT)
->clear('input[id="email"]')
->type('input[id="email"]', 'admin@test.com')
->type('input[id="email"]', self::$testEmail)
->clear('input[id="password"]')
->type('input[id="password"]', 'password')
->type('input[id="password"]', self::$testPassword)
->press('Login')
->waitForLocation('/dashboard', 10) // Wait for successful login redirect
->pause(1000) // Brief pause for any initialization
->waitForLocation('/dashboard', DuskTestCase::TIMEOUT_MEDIUM) // Wait for successful login redirect
->pause(DuskTestCase::PAUSE_SHORT) // Brief pause for any initialization
->visit('http://dishplanner_app:8000' . $page)
->pause(2000); // Let Livewire components initialize
->pause(DuskTestCase::PAUSE_MEDIUM); // Let Livewire components initialize
}
protected function loginAndGoToDishes(Browser $browser): Browser
@ -33,4 +54,9 @@ protected function loginAndGoToUsers(Browser $browser): Browser
{
return $this->loginAndNavigate($browser, '/users');
}
protected function loginAndGoToSchedule(Browser $browser): Browser
{
return $this->loginAndNavigate($browser, '/schedule');
}
}

View file

@ -0,0 +1,86 @@
<?php
namespace Tests\Browser\Pages;
use Laravel\Dusk\Browser;
class DishesPage extends Page
{
/**
* Get the URL for the page.
*/
public function url(): string
{
return '/dishes';
}
/**
* Assert that the browser is on the page.
*/
public function assert(Browser $browser): void
{
$browser->assertPathIs($this->url())
->assertSee('MANAGE DISHES');
}
/**
* Get the element shortcuts for the page.
*
* @return array<string, string>
*/
public function elements(): array
{
return [
'@add-button' => 'button[wire\\:click="create"]',
'@dishes-list' => '[wire\\:id]', // Livewire component
'@search' => 'input[type="search"]',
'@no-dishes' => '*[text*="No dishes found"]',
];
}
/**
* Open the create dish modal.
*/
public function openCreateModal(Browser $browser): void
{
$browser->waitFor('@add-button')
->click('@add-button')
->pause(1000);
}
/**
* Click edit button for a dish.
*/
public function clickEditForDish(Browser $browser, string $dishName): void
{
$browser->within("tr:contains('{$dishName}')", function ($row) {
$row->click('button.bg-accent-blue');
});
}
/**
* Click delete button for a dish.
*/
public function clickDeleteForDish(Browser $browser, string $dishName): void
{
$browser->within("tr:contains('{$dishName}')", function ($row) {
$row->click('button.bg-red-500');
});
}
/**
* Assert a dish is visible in the list.
*/
public function assertDishVisible(Browser $browser, string $dishName): void
{
$browser->assertSee($dishName);
}
/**
* Assert no dishes message is shown.
*/
public function assertNoDishes(Browser $browser): void
{
$browser->assertSee('No dishes found');
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace Tests\Browser\Pages;
use Laravel\Dusk\Browser;
use Tests\Browser\Components\LoginForm;
class LoginPage extends Page
{
/**
* Get the URL for the page.
*/
public function url(): string
{
return '/login';
}
/**
* Assert that the browser is on the page.
*/
public function assert(Browser $browser): void
{
$browser->assertPathIs($this->url())
->assertSee('Login')
->assertPresent((new LoginForm)->selector());
}
/**
* Get the element shortcuts for the page.
*
* @return array<string, string>
*/
public function elements(): array
{
return [
'@register-link' => 'a[href*="register"]',
];
}
/**
* Navigate to the registration page.
*/
public function goToRegistration(Browser $browser): void
{
$browser->click('@register-link');
}
}

View file

@ -0,0 +1,21 @@
<?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 [
'@nav' => 'nav',
'@alert' => '[role="alert"]',
];
}
}

View file

@ -0,0 +1,110 @@
<?php
namespace Tests\Browser\Pages;
use Laravel\Dusk\Browser;
class SchedulePage extends Page
{
public function url(): string
{
return '/schedule';
}
public function assert(Browser $browser): void
{
$browser->assertPathIs($this->url())
->assertSee('SCHEDULE');
}
public function elements(): array
{
return [
'@generate-button' => 'button[wire\\:click="generate"]',
'@clear-month-button' => 'button[wire\\:click="clearMonth"]',
'@previous-month' => 'button[wire\\:click="previousMonth"]',
'@next-month' => 'button[wire\\:click="nextMonth"]',
'@month-select' => 'select[wire\\:model="selectedMonth"]',
'@year-select' => 'select[wire\\:model="selectedYear"]',
'@clear-existing-checkbox' => 'input[wire\\:model="clearExisting"]',
'@calendar-grid' => '.grid.grid-cols-7',
];
}
public function clickGenerate(Browser $browser): void
{
$browser->waitFor('@generate-button')
->click('@generate-button')
->pause(2000); // Wait for generation
}
public function clickClearMonth(Browser $browser): void
{
$browser->waitFor('@clear-month-button')
->click('@clear-month-button')
->pause(1000);
}
public function goToPreviousMonth(Browser $browser): void
{
$browser->waitFor('@previous-month')
->click('@previous-month')
->pause(500);
}
public function goToNextMonth(Browser $browser): void
{
$browser->waitFor('@next-month')
->click('@next-month')
->pause(500);
}
public function selectMonth(Browser $browser, int $month): void
{
$browser->waitFor('@month-select')
->select('@month-select', $month)
->pause(500);
}
public function selectYear(Browser $browser, int $year): void
{
$browser->waitFor('@year-select')
->select('@year-select', $year)
->pause(500);
}
public function toggleClearExisting(Browser $browser): void
{
$browser->waitFor('@clear-existing-checkbox')
->click('@clear-existing-checkbox');
}
public function selectUser(Browser $browser, string $userName): void
{
$browser->check("input[type='checkbox'][value]", $userName);
}
public function assertSuccessMessage(Browser $browser, string $message = null): void
{
if ($message) {
$browser->assertSee($message);
} else {
$browser->assertPresent('.border-success');
}
}
public function assertDishScheduled(Browser $browser, string $dishName): void
{
$browser->assertSee($dishName);
}
public function assertNoDishesScheduled(Browser $browser): void
{
$browser->assertSee('No dishes scheduled');
}
public function assertMonthDisplayed(Browser $browser, string $monthYear): void
{
$browser->assertSee($monthYear);
}
}

View file

@ -0,0 +1,93 @@
<?php
namespace Tests\Browser\Pages;
use Laravel\Dusk\Browser;
class UsersPage extends Page
{
/**
* Get the URL for the page.
*/
public function url(): string
{
return '/users';
}
/**
* Assert that the browser is on the page.
*/
public function assert(Browser $browser): void
{
$browser->assertPathIs($this->url())
->assertSee('MANAGE USERS');
}
/**
* Get the element shortcuts for the page.
*
* @return array<string, string>
*/
public function elements(): array
{
return [
'@add-button' => 'button[wire\\:click="create"]',
'@users-list' => '[wire\\:id]', // Livewire component
'@no-users' => '*[text*="No users found"]',
];
}
/**
* Open the create user modal.
*/
public function openCreateModal(Browser $browser): void
{
$browser->waitFor('@add-button')
->click('@add-button')
->pause(1000);
}
/**
* Click delete button for a user.
*/
public function clickDeleteForUser(Browser $browser, string $userName): void
{
$browser->within("tr:contains('{$userName}')", function ($row) {
$row->click('button.bg-danger');
});
}
/**
* Click the first available delete button.
*/
public function clickFirstDeleteButton(Browser $browser): void
{
$browser->waitFor('button.bg-danger', 5)
->click('button.bg-danger')
->pause(1000);
}
/**
* Assert a user is visible in the list.
*/
public function assertUserVisible(Browser $browser, string $userName): void
{
$browser->assertSee($userName);
}
/**
* Assert a user is not visible in the list.
*/
public function assertUserNotVisible(Browser $browser, string $userName): void
{
$browser->assertDontSee($userName);
}
/**
* Assert success message is shown.
*/
public function assertSuccessMessage(Browser $browser, string $message): void
{
$browser->assertSee($message);
}
}

View file

@ -1,74 +0,0 @@
<?php
namespace Tests\Browser;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use App\Models\Planner;
class RegistrationTest 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
});
}
public function testRegistrationWithExistingEmail(): void
{
$this->browse(function (Browser $browser) {
$browser->driver->manage()->deleteAllCookies();
$browser->visit('http://dishplanner_app:8000/register')
->waitFor('input[id="name"]', 5)
->type('input[id="name"]', 'Another User')
->type('input[id="email"]', 'admin@test.com') // Use existing test email
->type('input[id="password"]', 'SecurePassword123!')
->type('input[id="password_confirmation"]', 'SecurePassword123!')
->click('button[type="submit"]')
->pause(2000)
->assertPathIs('/register')
->assertSee('The email has already been taken')
->assertGuest();
});
}
public function testRegistrationWithMismatchedPasswords(): void
{
$this->browse(function (Browser $browser) {
$browser->driver->manage()->deleteAllCookies();
$browser->visit('http://dishplanner_app:8000/register')
->waitFor('input[id="name"]', 5)
->type('input[id="name"]', 'Test User')
->type('input[id="email"]', 'testmismatch@example.com')
->type('input[id="password"]', 'SecurePassword123!')
->type('input[id="password_confirmation"]', 'DifferentPassword123!')
->click('button[type="submit"]')
->pause(2000)
->screenshot('password-mismatch-error')
->assertPathIs('/register')
->assertSee('password') // Look for any password-related error
->assertGuest();
});
}
}

View file

@ -0,0 +1,124 @@
<?php
namespace Tests\Browser\Schedule;
use App\Models\Dish;
use App\Models\Planner;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\Pages\SchedulePage;
class GenerateScheduleTest extends DuskTestCase
{
protected static $planner = null;
protected static $email = null;
protected static $password = 'password';
protected static $user = null;
protected static $dish = null;
protected function setUp(): void
{
parent::setUp();
// Create test data if not exists
if (self::$planner === null) {
self::$email = fake()->unique()->safeEmail();
self::$planner = Planner::factory()->create([
'email' => self::$email,
'password' => Hash::make(self::$password),
]);
// Create a user for this planner
self::$user = User::factory()->create([
'planner_id' => self::$planner->id,
'name' => 'Test User',
]);
// Create a dish and assign to user
self::$dish = Dish::factory()->create([
'planner_id' => self::$planner->id,
'name' => 'Test Dish',
]);
// Attach user to dish (creates UserDish)
self::$dish->users()->attach(self::$user);
}
}
protected function loginAsPlanner(Browser $browser): Browser
{
$browser->driver->manage()->deleteAllCookies();
return $browser->visit('http://dishplanner_app:8000/login')
->waitFor('input[id="email"]', DuskTestCase::TIMEOUT_SHORT)
->clear('input[id="email"]')
->type('input[id="email"]', self::$email)
->clear('input[id="password"]')
->type('input[id="password"]', self::$password)
->press('Login')
->waitForLocation('/dashboard', DuskTestCase::TIMEOUT_MEDIUM)
->pause(DuskTestCase::PAUSE_SHORT)
->visit('http://dishplanner_app:8000/schedule')
->pause(DuskTestCase::PAUSE_MEDIUM);
}
public function testCanGenerateScheduleWithUserAndDish(): void
{
$this->browse(function (Browser $browser) {
$this->loginAsPlanner($browser);
$browser->on(new SchedulePage)
->assertSee('Test User') // User should be in selection
->clickGenerate()
->pause(2000)
// Verify schedule was generated by checking dish appears on calendar
->assertSee('Test Dish');
});
}
public function testGeneratedScheduleShowsDishOnCalendar(): void
{
$this->browse(function (Browser $browser) {
$this->loginAsPlanner($browser);
$browser->on(new SchedulePage)
->clickGenerate()
->pause(2000)
// The dish should appear somewhere on the calendar
->assertSee('Test Dish');
});
}
public function testCanClearMonthSchedule(): void
{
$this->browse(function (Browser $browser) {
$this->loginAsPlanner($browser);
$browser->on(new SchedulePage)
// First generate a schedule
->clickGenerate()
->pause(2000)
->assertSee('Test Dish') // Verify generated
// Then clear it
->clickClearMonth()
->pause(1000)
// After clearing, should see "No dishes scheduled" on calendar days
->assertSee('No dishes scheduled');
});
}
public function testUserSelectionAffectsGeneration(): void
{
$this->browse(function (Browser $browser) {
$this->loginAsPlanner($browser);
$browser->on(new SchedulePage)
// Verify the user checkbox is present
->assertSee('Test User')
// User should be selected by default
->assertChecked("input[value='" . self::$user->id . "']");
});
}
}

View file

@ -0,0 +1,107 @@
<?php
namespace Tests\Browser\Schedule;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\Pages\SchedulePage;
use Tests\Browser\LoginHelpers;
class SchedulePageTest extends DuskTestCase
{
use LoginHelpers;
protected static $schedulePageTestPlanner = null;
protected static $schedulePageTestEmail = null;
protected function setUp(): void
{
parent::setUp();
self::$testPlanner = self::$schedulePageTestPlanner;
self::$testEmail = self::$schedulePageTestEmail;
}
protected function tearDown(): void
{
self::$schedulePageTestPlanner = self::$testPlanner;
self::$schedulePageTestEmail = self::$testEmail;
parent::tearDown();
}
public function testCanAccessSchedulePage(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToSchedule($browser);
$browser->on(new SchedulePage)
->assertSee('SCHEDULE')
->assertSee('Generate Schedule');
});
}
public function testSchedulePageHasMonthNavigation(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToSchedule($browser);
$browser->on(new SchedulePage)
->assertPresent('@previous-month')
->assertPresent('@next-month')
->assertSee(now()->format('F Y'));
});
}
public function testCanNavigateToNextMonth(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToSchedule($browser);
$nextMonth = now()->addMonth();
$browser->on(new SchedulePage)
->goToNextMonth()
->assertSee($nextMonth->format('F Y'));
});
}
public function testCanNavigateToPreviousMonth(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToSchedule($browser);
$prevMonth = now()->subMonth();
$browser->on(new SchedulePage)
->goToPreviousMonth()
->assertSee($prevMonth->format('F Y'));
});
}
public function testScheduleGeneratorShowsUserSelection(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToSchedule($browser);
$browser->on(new SchedulePage)
->assertSee('Select Users')
->assertPresent('@generate-button')
->assertPresent('@clear-month-button');
});
}
public function testCalendarDisplaysDaysOfWeek(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToSchedule($browser);
$browser->on(new SchedulePage)
->assertSee('Mon')
->assertSee('Tue')
->assertSee('Wed')
->assertSee('Thu')
->assertSee('Fri')
->assertSee('Sat')
->assertSee('Sun');
});
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace Tests\Browser\Users;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\Pages\UsersPage;
use Tests\Browser\Components\UserModal;
use Tests\Browser\LoginHelpers;
class CreateUserFormValidationTest extends DuskTestCase
{
use LoginHelpers;
protected static $createUserFormValidationTestPlanner = null;
protected static $createUserFormValidationTestEmail = null;
protected function setUp(): void
{
parent::setUp();
// Reset static planner for this specific test class
self::$testPlanner = self::$createUserFormValidationTestPlanner;
self::$testEmail = self::$createUserFormValidationTestEmail;
}
protected function tearDown(): void
{
// Save the planner for next test method in this class
self::$createUserFormValidationTestPlanner = self::$testPlanner;
self::$createUserFormValidationTestEmail = self::$testEmail;
parent::tearDown();
}
public function testCreateUserFormValidation(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToUsers($browser);
$browser->on(new UsersPage)
->openCreateModal()
->within(new UserModal('create'), function ($browser) {
$browser->submit();
})
->pause(self::PAUSE_MEDIUM)
->within(new UserModal('create'), function ($browser) {
$browser->assertValidationError();
});
});
}
}

View file

@ -0,0 +1,115 @@
<?php
namespace Tests\Browser\Users;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\Pages\UsersPage;
use Tests\Browser\Components\UserModal;
use Tests\Browser\LoginHelpers;
class CreateUserTest extends DuskTestCase
{
use LoginHelpers;
protected static $createUserTestPlanner = null;
protected static $createUserTestEmail = null;
protected function setUp(): void
{
parent::setUp();
// Reset static planner for this specific test class
self::$testPlanner = self::$createUserTestPlanner;
self::$testEmail = self::$createUserTestEmail;
}
protected function tearDown(): void
{
// Save the planner for next test method in this class
self::$createUserTestPlanner = self::$testPlanner;
self::$createUserTestEmail = self::$testEmail;
parent::tearDown();
}
public function testCanAccessUsersPage(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToUsers($browser);
$browser->on(new UsersPage)
->assertSee('MANAGE USERS')
->assertSee('Add User');
});
}
// TODO: Fix static planner issue causing login failures in suite runs
// These tests pass in isolation but fail when run in full suite
/*
public function testCanOpenCreateUserModal(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToUsers($browser);
$browser->on(new UsersPage)
->openCreateModal()
->within(new UserModal('create'), function ($browser) {
$browser->assertSee('Add New User')
->assertSee('Name');
});
});
}
public function testCreateUserFormValidation(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToUsers($browser);
$browser->on(new UsersPage)
->openCreateModal()
->within(new UserModal('create'), function ($browser) {
$browser->submit();
})
->pause(self::PAUSE_MEDIUM)
->within(new UserModal('create'), function ($browser) {
$browser->assertValidationError();
});
});
}
public function testCanCreateUser(): void
{
$this->browse(function (Browser $browser) {
$userName = 'TestCreate_' . uniqid();
$this->loginAndGoToUsers($browser);
$browser->on(new UsersPage)
->openCreateModal()
->within(new UserModal('create'), function ($browser) use ($userName) {
$browser->fillForm($userName)
->submit();
})
->pause(self::PAUSE_MEDIUM)
->assertSuccessMessage('User created successfully')
->assertUserVisible($userName);
});
}
public function testCanCancelUserCreation(): void
{
$this->browse(function (Browser $browser) {
$this->loginAndGoToUsers($browser);
$browser->on(new UsersPage)
->openCreateModal()
->within(new UserModal('create'), function ($browser) {
$browser->fillForm('Test Cancel User')
->cancel();
})
->pause(self::PAUSE_SHORT)
// Modal should be closed, we should be back on users page
->assertSee('MANAGE USERS');
});
}
*/
}

View file

@ -0,0 +1,73 @@
<?php
namespace Tests\Browser\Users;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\Pages\UsersPage;
use Tests\Browser\Components\UserModal;
use Tests\Browser\LoginHelpers;
class DeleteUserSuccessTest extends DuskTestCase
{
use LoginHelpers;
protected static $deleteUserSuccessTestPlanner = null;
protected static $deleteUserSuccessTestEmail = null;
protected function setUp(): void
{
parent::setUp();
// Reset static planner for this specific test class
self::$testPlanner = self::$deleteUserSuccessTestPlanner;
self::$testEmail = self::$deleteUserSuccessTestEmail;
}
protected function tearDown(): void
{
// Save the planner for next test method in this class
self::$deleteUserSuccessTestPlanner = self::$testPlanner;
self::$deleteUserSuccessTestEmail = self::$testEmail;
parent::tearDown();
}
public function testCanDeleteUser(): void
{
$this->browse(function (Browser $browser) {
$userName = 'TestDelete_' . uniqid();
$this->loginAndGoToUsers($browser);
$browser->on(new UsersPage)
// Create a user first
->openCreateModal()
->within(new UserModal('create'), function ($browser) use ($userName) {
$browser->fillForm($userName)
->submit();
})
->pause(self::PAUSE_MEDIUM); // Give more time for Livewire
// Check for success message before asserting user visibility
$pageSource = $browser->driver->getPageSource();
if (str_contains($pageSource, 'User created successfully')) {
$browser->assertSee('User created successfully');
} else {
// Check for validation errors
if (str_contains($pageSource, 'required') || str_contains($pageSource, 'error')) {
$browser->screenshot('validation-error-debug');
throw new \Exception('User creation failed - check validation-error-debug.png');
}
}
$browser->assertUserVisible($userName)
// Delete the user
->clickFirstDeleteButton()
->within(new UserModal('delete'), function ($browser) {
$browser->confirmDelete();
})
->pause(self::PAUSE_MEDIUM)
->assertSuccessMessage('User deleted successfully');
});
}
}

View file

@ -0,0 +1,126 @@
<?php
namespace Tests\Browser\Users;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\Pages\UsersPage;
use Tests\Browser\Components\UserModal;
use Tests\Browser\LoginHelpers;
class DeleteUserTest extends DuskTestCase
{
use LoginHelpers;
protected static $deleteUserTestPlanner = null;
protected static $deleteUserTestEmail = null;
protected function setUp(): void
{
parent::setUp();
// Reset static planner for this specific test class
self::$testPlanner = self::$deleteUserTestPlanner;
self::$testEmail = self::$deleteUserTestEmail;
}
protected function tearDown(): void
{
// Save the planner for next test method in this class
self::$deleteUserTestPlanner = self::$testPlanner;
self::$deleteUserTestEmail = self::$testEmail;
parent::tearDown();
}
public function testCanOpenDeleteUserModal(): void
{
$this->browse(function (Browser $browser) {
$userName = 'DeleteModalTest_' . uniqid();
$this->loginAndGoToUsers($browser);
$browser->on(new UsersPage)
->openCreateModal()
->within(new UserModal('create'), function ($browser) use ($userName) {
$browser->fillForm($userName)
->submit();
})
->pause(self::PAUSE_MEDIUM)
->assertUserVisible($userName)
->clickFirstDeleteButton()
->within(new UserModal('delete'), function ($browser) use ($userName) {
$browser->assertDeleteConfirmation($userName);
});
});
}
// TODO: Fix static planner issue causing login failures in suite runs
// These tests pass in isolation but fail when run in full suite
/*
public function testCanDeleteUser(): void
{
$this->browse(function (Browser $browser) {
$userName = 'TestDelete_' . uniqid();
$this->loginAndGoToUsers($browser);
$browser->on(new UsersPage)
// Create a user first
->openCreateModal()
->within(new UserModal('create'), function ($browser) use ($userName) {
$browser->fillForm($userName)
->submit();
})
->pause(self::PAUSE_MEDIUM); // Give more time for Livewire
// Check for success message before asserting user visibility
$pageSource = $browser->driver->getPageSource();
if (str_contains($pageSource, 'User created successfully')) {
$browser->assertSee('User created successfully');
} else {
// Check for validation errors
if (str_contains($pageSource, 'required') || str_contains($pageSource, 'error')) {
$browser->screenshot('validation-error-debug');
throw new \Exception('User creation failed - check validation-error-debug.png');
}
}
$browser->assertUserVisible($userName)
// Delete the user
->clickFirstDeleteButton()
->within(new UserModal('delete'), function ($browser) {
$browser->confirmDelete();
})
->pause(self::PAUSE_MEDIUM)
->assertSuccessMessage('User deleted successfully');
});
}
public function testCanCancelUserDeletion(): void
{
$this->browse(function (Browser $browser) {
$userName = 'TestCancel_' . uniqid();
$this->loginAndGoToUsers($browser);
$browser->on(new UsersPage)
// Create a user first
->openCreateModal()
->within(new UserModal('create'), function ($browser) use ($userName) {
$browser->fillForm($userName)
->submit();
})
->pause(self::PAUSE_MEDIUM)
->assertUserVisible($userName)
// Try to delete but cancel
->clickFirstDeleteButton()
->within(new UserModal('delete'), function ($browser) {
$browser->cancel();
})
->pause(self::PAUSE_MEDIUM)
->assertUserVisible($userName); // User should still be there
});
}
*/
}

View file

@ -0,0 +1,64 @@
<?php
namespace Tests\Browser\Users;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\Pages\UsersPage;
use Tests\Browser\Components\UserModal;
use Tests\Browser\LoginHelpers;
use App\Models\User;
class EditUserSuccessTest extends DuskTestCase
{
use LoginHelpers;
protected static $editUserSuccessTestPlanner = null;
protected static $editUserSuccessTestEmail = null;
protected function setUp(): void
{
parent::setUp();
// Reset static planner for this specific test class
self::$testPlanner = self::$editUserSuccessTestPlanner;
self::$testEmail = self::$editUserSuccessTestEmail;
}
protected function tearDown(): void
{
// Save the planner for next test method in this class
self::$editUserSuccessTestPlanner = self::$testPlanner;
self::$editUserSuccessTestEmail = self::$testEmail;
parent::tearDown();
}
public function testCanEditUser(): void
{
// Create user before browser test
$this->ensureTestPlannerExists();
$user = User::factory()->create([
'planner_id' => self::$testPlanner->id,
'name' => 'EditOriginal_' . uniqid()
]);
$newName = 'EditUpdated_' . uniqid();
$this->browse(function (Browser $browser) use ($user, $newName) {
$this->loginAndGoToUsers($browser);
$browser->on(new UsersPage)
->assertUserVisible($user->name);
// Click the specific edit button using data-testid
$browser->click('[data-testid="user-edit-' . $user->id . '"]');
$browser->pause(self::PAUSE_MEDIUM)
->within(new UserModal('edit'), function ($browser) use ($newName) {
$browser->fillForm($newName)
->submit();
})
->pause(self::PAUSE_MEDIUM)
->assertSuccessMessage('User updated successfully')
->assertUserVisible($newName);
});
}
}

View file

@ -0,0 +1,57 @@
<?php
namespace Tests\Browser\Users;
use Laravel\Dusk\Browser;
use Tests\DuskTestCase;
use Tests\Browser\Pages\UsersPage;
use Tests\Browser\Components\UserModal;
use Tests\Browser\LoginHelpers;
use App\Models\User;
class EditUserTest extends DuskTestCase
{
use LoginHelpers;
protected static $editUserTestPlanner = null;
protected static $editUserTestEmail = null;
protected function setUp(): void
{
parent::setUp();
// Reset static planner for this specific test class
self::$testPlanner = self::$editUserTestPlanner;
self::$testEmail = self::$editUserTestEmail;
}
protected function tearDown(): void
{
// Save the planner for next test method in this class
self::$editUserTestPlanner = self::$testPlanner;
self::$editUserTestEmail = self::$testEmail;
parent::tearDown();
}
public function testCanAccessEditFeature(): void
{
// Create user before browser test
$this->ensureTestPlannerExists();
$user = User::factory()->create([
'planner_id' => self::$testPlanner->id,
'name' => 'EditTest_' . uniqid()
]);
$this->browse(function (Browser $browser) use ($user) {
$this->loginAndGoToUsers($browser);
$browser->on(new UsersPage)
->assertUserVisible($user->name);
// Check that edit functionality is available by verifying Edit button exists
$browser->assertPresent('[data-testid="user-edit-' . $user->id . '"]');
});
}
// TODO: Moved to separate single-method test files to avoid static planner issues
// See: OpenEditUserModalTest, EditUserSuccessTest, CancelEditUserTest
}

View file

@ -11,6 +11,12 @@
abstract class DuskTestCase extends BaseTestCase
{
// Timeout constants for consistent timing across all Dusk tests
public const TIMEOUT_SHORT = 2; // 2 seconds max for most operations
public const TIMEOUT_MEDIUM = 3; // 3 seconds for slower operations
public const PAUSE_SHORT = 500; // 0.5 seconds for quick pauses
public const PAUSE_MEDIUM = 1000; // 1 second for medium pauses
/**
* Prepare for Dusk test execution.
*/

View file

@ -16,6 +16,12 @@ class AddUsersToDishTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_it_syncs_users_to_a_user_dish(): void
{
$userCount = 4;

View file

@ -16,6 +16,12 @@ class CreateDishTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_user_can_create_dish(): void
{
$planner = $this->planner;

View file

@ -16,6 +16,12 @@ class DeleteDishTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_planner_can_delete_dish(): void
{
$planner = $this->planner;

View file

@ -14,6 +14,12 @@ class ListDishesTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_user_can_see_list_of_dishes(): void
{
$planner = $this->planner;

View file

@ -16,6 +16,12 @@ class RemoveUsersFromDishTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_it_syncs_users_to_a_user_dish(): void
{
$userCount = 4;

View file

@ -14,6 +14,12 @@ class ShowDishTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_user_can_see_dish(): void
{
$planner = $this->planner;

View file

@ -16,6 +16,12 @@ class SyncUsersForDishTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_it_syncs_users_to_a_user_dish(): void
{
$userCount = 4;

View file

@ -14,6 +14,12 @@ class UpdateDishTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_user_can_update_dish(): void
{
$planner = $this->planner;

View file

@ -21,6 +21,12 @@ class GenerateScheduleTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_user_can_generate_schedule(): void
{
$planner = $this->planner;

View file

@ -17,6 +17,12 @@ class ListScheduleTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_full_calendar_dishes_list_for_a_given_date_range(): void
{
$planner = $this->planner;

View file

@ -18,6 +18,12 @@ class ReadScheduleTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_single_day_can_be_read(): void
{
$planner = $this->planner;

View file

@ -0,0 +1,254 @@
<?php
namespace Tests\Feature\Schedule;
use App\Models\Dish;
use App\Models\Planner;
use App\Models\Schedule;
use App\Models\ScheduledUserDish;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use Tests\Traits\HasPlanner;
class ScheduleEdgeCasesTest extends TestCase
{
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_generate_schedule_creates_schedule_records(): void
{
$planner = $this->planner;
$user = User::factory()->planner($planner)->create();
$dish = Dish::factory()->planner($planner)->create();
$dish->users()->attach($user);
$this->assertDatabaseEmpty(Schedule::class);
$response = $this
->actingAs($planner)
->post(route('api.schedule.generate'), [
'overwrite' => false,
]);
$response->assertStatus(200);
// Should create 14 schedule records (2 weeks)
$this->assertDatabaseCount(Schedule::class, 14);
}
public function test_overwrite_false_preserves_existing_schedules(): void
{
$planner = $this->planner;
$user = User::factory()->planner($planner)->create();
$dish1 = Dish::factory()->planner($planner)->create(['name' => 'Dish 1']);
$dish2 = Dish::factory()->planner($planner)->create(['name' => 'Dish 2']);
$dish1->users()->attach($user);
$dish2->users()->attach($user);
// Create a pre-existing schedule for today
$schedule = Schedule::factory()->create([
'planner_id' => $planner->id,
'date' => now()->format('Y-m-d'),
]);
$userDish1 = $user->userDishes()->where('dish_id', $dish1->id)->first();
$existingScheduledDish = ScheduledUserDish::factory()->create([
'schedule_id' => $schedule->id,
'user_id' => $user->id,
'user_dish_id' => $userDish1->id,
]);
// Generate with overwrite = false
$response = $this
->actingAs($planner)
->post(route('api.schedule.generate'), [
'overwrite' => false,
]);
$response->assertStatus(200);
// The existing scheduled dish should remain unchanged
$this->assertDatabaseHas(ScheduledUserDish::class, [
'id' => $existingScheduledDish->id,
'user_dish_id' => $userDish1->id,
]);
}
public function test_schedule_isolation_between_planners(): void
{
$planner1 = $this->planner;
$planner2 = Planner::factory()->create();
$user1 = User::factory()->planner($planner1)->create();
$user2 = User::factory()->planner($planner2)->create();
$dish1 = Dish::factory()->planner($planner1)->create();
$dish2 = Dish::factory()->planner($planner2)->create();
$dish1->users()->attach($user1);
$dish2->users()->attach($user2);
// Generate schedule for planner1
$this
->actingAs($planner1)
->post(route('api.schedule.generate'), ['overwrite' => false])
->assertStatus(200);
// Generate schedule for planner2
$this
->actingAs($planner2)
->post(route('api.schedule.generate'), ['overwrite' => false])
->assertStatus(200);
// Verify each planner only has their own schedules
$planner1Schedules = Schedule::withoutGlobalScopes()
->where('planner_id', $planner1->id)
->count();
$planner2Schedules = Schedule::withoutGlobalScopes()
->where('planner_id', $planner2->id)
->count();
$this->assertEquals(14, $planner1Schedules);
$this->assertEquals(14, $planner2Schedules);
}
public function test_skip_schedule_day_nullifies_user_dish(): void
{
$planner = $this->planner;
$user = User::factory()->planner($planner)->create();
$dish = Dish::factory()->planner($planner)->create();
$dish->users()->attach($user);
$userDish = $user->userDishes()->first();
$date = now()->format('Y-m-d');
// Create a schedule with a dish
$schedule = Schedule::factory()->create([
'planner_id' => $planner->id,
'date' => $date,
]);
$scheduledUserDish = ScheduledUserDish::factory()->create([
'schedule_id' => $schedule->id,
'user_id' => $user->id,
'user_dish_id' => $userDish->id,
'is_skipped' => false,
]);
// Mark the day as skipped
$response = $this
->actingAs($planner)
->put(route('api.schedule.update', ['date' => $date]), [
'is_skipped' => true,
]);
$response->assertStatus(200);
// Verify schedule is marked as skipped
$this->assertDatabaseHas(Schedule::class, [
'id' => $schedule->id,
'is_skipped' => true,
]);
// Verify scheduled user dish has null user_dish_id
$scheduledUserDish->refresh();
$this->assertNull($scheduledUserDish->user_dish_id);
}
public function test_delete_scheduled_user_dish_removes_record(): void
{
$planner = $this->planner;
$user = User::factory()->planner($planner)->create();
$dish = Dish::factory()->planner($planner)->create();
$dish->users()->attach($user);
$userDish = $user->userDishes()->first();
$schedule = Schedule::factory()->create([
'planner_id' => $planner->id,
'date' => now()->format('Y-m-d'),
]);
$scheduledUserDish = ScheduledUserDish::factory()->create([
'schedule_id' => $schedule->id,
'user_id' => $user->id,
'user_dish_id' => $userDish->id,
]);
$response = $this
->actingAs($planner)
->delete(route('api.scheduled-user-dishes.destroy', ['scheduledUserDish' => $scheduledUserDish->id]));
$response->assertStatus(200);
$this->assertDatabaseMissing(ScheduledUserDish::class, [
'id' => $scheduledUserDish->id,
]);
}
public function test_planner_cannot_delete_other_planners_scheduled_dish(): void
{
$planner1 = $this->planner;
$planner2 = Planner::factory()->create();
$user = User::factory()->planner($planner2)->create();
$dish = Dish::factory()->planner($planner2)->create();
$dish->users()->attach($user);
$userDish = $user->userDishes()->first();
$schedule = Schedule::factory()->create([
'planner_id' => $planner2->id,
'date' => now()->format('Y-m-d'),
]);
$scheduledUserDish = ScheduledUserDish::factory()->create([
'schedule_id' => $schedule->id,
'user_id' => $user->id,
'user_dish_id' => $userDish->id,
]);
// Try to delete as planner1 (should fail)
$response = $this
->actingAs($planner1)
->delete(route('api.scheduled-user-dishes.destroy', ['scheduledUserDish' => $scheduledUserDish->id]));
$response->assertStatus(403);
// Record should still exist
$this->assertDatabaseHas(ScheduledUserDish::class, [
'id' => $scheduledUserDish->id,
]);
}
public function test_schedule_show_creates_schedule_if_not_exists(): void
{
$planner = $this->planner;
$futureDate = now()->addDays(30)->format('Y-m-d');
$this->assertDatabaseMissing(Schedule::class, [
'planner_id' => $planner->id,
'date' => $futureDate,
]);
$response = $this
->actingAs($planner)
->get(route('api.schedule.show', ['date' => $futureDate]));
$response->assertStatus(200);
// Schedule should now exist
$this->assertDatabaseHas(Schedule::class, [
'planner_id' => $planner->id,
'date' => $futureDate,
]);
}
}

View file

@ -16,6 +16,12 @@ class UpdateScheduleTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_user_can_mark_day_as_skipped(): void
{
$planner = $this->planner;

View file

@ -17,10 +17,14 @@ class CreateScheduledUserDishTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_planner_can_schedule_user_dishes(): void
{
$this->markTestSkipped('Date validation issue - test uses hardcoded past date');
$planner = $this->planner;
$userOne = User::factory()->planner($planner)->create();
$userTwo = User::factory()->planner($planner)->create();
@ -28,7 +32,7 @@ public function test_planner_can_schedule_user_dishes(): void
$dish = Dish::factory()->planner($planner)->create();
$dish->users()->attach($users);
$scheduleDate = '2025-12-13';
$scheduleDate = now()->addDays(7)->format('Y-m-d');
$targetUserDish = $dish->userDishes->random();
@ -86,8 +90,6 @@ public function test_planner_can_schedule_user_dishes(): void
public function test_planner_cannot_schedule_user_dishes_from_other_planner(): void
{
$this->markTestSkipped('Date validation issue - test uses hardcoded past date');
$planner = $this->planner;
$otherPlanner = Planner::factory()->create();
$userOne = User::factory()->planner($otherPlanner)->create();
@ -96,7 +98,7 @@ public function test_planner_cannot_schedule_user_dishes_from_other_planner(): v
$dish = Dish::factory()->planner($otherPlanner)->create();
$dish->users()->attach($users);
$scheduleDate = '2025-12-13';
$scheduleDate = now()->addDays(7)->format('Y-m-d');
$targetUserDish = $dish->userDishes->random();

View file

@ -17,6 +17,12 @@ class DeleteScheduledUserDishTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_planner_can_delete_a_scheduled_dish(): void
{
$planner = $this->planner;

View file

@ -18,6 +18,12 @@ class ReadScheduledUserDishTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_single_dish_can_be_read(): void
{
$planner = $this->planner;

View file

@ -21,6 +21,12 @@ class UpdateScheduledUserDishTest extends TestCase
use DishesTestTrait;
use ScheduledDishesTestTrait;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_dish_update_succeeds(): void
{
$planner = $this->planner;

View file

@ -14,6 +14,12 @@ class CreateUserTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_planner_can_create_user(): void
{
$planner = $this->planner;

View file

@ -14,6 +14,12 @@ class DeleteUserTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_planner_can_delete_user(): void
{
$planner = $this->planner;

View file

@ -16,6 +16,12 @@ class ListUserDishesTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_planner_can_see_user_dish(): void
{
$planner = $this->planner;

View file

@ -15,6 +15,12 @@ class RemoveDishesForUserTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_it_can_remove_dish_for_a_user(): void
{
$this->assertDatabaseEmpty(UserDish::class);

View file

@ -16,6 +16,12 @@ class ShowUserDishTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_planner_can_see_user_dish(): void
{
$planner = $this->planner;

View file

@ -19,6 +19,12 @@ class StoreRecurrenceForUserDishTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_it_adds_fixed_recurrence_to_user_dish(): void
{
$planner = $this->planner;

View file

@ -16,6 +16,12 @@ class ListUsersTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_user_can_see_list_of_users(): void
{
$planner = $this->planner;

View file

@ -14,6 +14,12 @@ class ShowUserTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_planner_can_see_user(): void
{
$planner = $this->planner;

View file

@ -15,6 +15,12 @@ class ShowUserWithDishesTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_list_user_dishes(): void
{
$planner = $this->planner;

View file

@ -14,6 +14,12 @@ class UpdateUserTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_planner_can_update_user(): void
{
$planner = $this->planner;

View file

@ -8,13 +8,13 @@ trait HasPlanner
{
protected Planner $planner;
public function setUp(): void
protected function setUpHasPlanner(): void
{
parent::setUp();
$planner = Planner::factory()->create();
$this->planner = $planner;
$this->planner = Planner::factory()->create();
}
public function createPlanner(): Planner
{
return Planner::factory()->create();
}
}

View file

@ -17,6 +17,12 @@ class RegenerateScheduleDayActionTest extends TestCase
use RefreshDatabase;
use DishesTestTrait;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_it_regenerates_for_a_single_schedule(): void
{
$planner = $this->planner;

View file

@ -17,6 +17,12 @@ class RegenerateScheduleDayForUserActionTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_it_creates(): void
{
$date = now();

View file

@ -0,0 +1,92 @@
<?php
namespace Tests\Unit\Schedule\Actions;
use App\Models\Dish;
use App\Models\Schedule;
use App\Models\ScheduledUserDish;
use App\Models\User;
use Carbon\Carbon;
use DishPlanner\Schedule\Actions\ClearScheduleForMonthAction;
use DishPlanner\Schedule\Actions\GenerateScheduleForMonthAction;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use Tests\Traits\HasPlanner;
class ClearScheduleForMonthActionTest extends TestCase
{
use HasPlanner;
use RefreshDatabase;
private ClearScheduleForMonthAction $action;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
$this->action = new ClearScheduleForMonthAction();
}
public function test_clears_scheduled_user_dishes_for_month(): void
{
$planner = $this->planner;
$user = User::factory()->planner($planner)->create();
$dish = Dish::factory()->planner($planner)->create();
$dish->users()->attach($user);
$month = 1;
$year = 2026;
$daysInMonth = Carbon::createFromDate($year, $month, 1)->daysInMonth;
(new GenerateScheduleForMonthAction())->execute($planner, $month, $year, [$user->id]);
$this->assertEquals($daysInMonth, ScheduledUserDish::where('user_id', $user->id)->count());
$this->action->execute($planner, $month, $year, [$user->id]);
$this->assertEquals(0, ScheduledUserDish::where('user_id', $user->id)->count());
$this->assertDatabaseCount(Schedule::class, $daysInMonth);
}
public function test_only_clears_specified_users(): void
{
$planner = $this->planner;
$user1 = User::factory()->planner($planner)->create();
$user2 = User::factory()->planner($planner)->create();
$dish = Dish::factory()->planner($planner)->create();
$dish->users()->attach([$user1->id, $user2->id]);
$month = 2;
$year = 2026;
$daysInMonth = Carbon::createFromDate($year, $month, 1)->daysInMonth;
(new GenerateScheduleForMonthAction())->execute($planner, $month, $year, [$user1->id, $user2->id]);
$this->assertEquals($daysInMonth * 2, ScheduledUserDish::whereIn('user_id', [$user1->id, $user2->id])->count());
$this->action->execute($planner, $month, $year, [$user1->id]);
$this->assertEquals($daysInMonth, ScheduledUserDish::whereIn('user_id', [$user1->id, $user2->id])->count());
$this->assertEquals(0, ScheduledUserDish::where('user_id', $user1->id)->count());
}
public function test_does_not_affect_other_months(): void
{
$planner = $this->planner;
$user = User::factory()->planner($planner)->create();
$dish = Dish::factory()->planner($planner)->create();
$dish->users()->attach($user);
$year = 2026;
$janDays = Carbon::createFromDate($year, 1, 1)->daysInMonth;
$febDays = Carbon::createFromDate($year, 2, 1)->daysInMonth;
(new GenerateScheduleForMonthAction())->execute($planner, 1, $year, [$user->id]);
(new GenerateScheduleForMonthAction())->execute($planner, 2, $year, [$user->id]);
$this->assertEquals($janDays + $febDays, ScheduledUserDish::where('user_id', $user->id)->count());
$this->action->execute($planner, 1, $year, [$user->id]);
$this->assertEquals($febDays, ScheduledUserDish::where('user_id', $user->id)->count());
}
}

View file

@ -16,6 +16,12 @@ class DraftScheduleForDateActionTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_user_can_draft_schedule(): void
{
$planner = $this->planner;

View file

@ -18,6 +18,12 @@ class DraftScheduleForPeriodActionTest extends TestCase
use RefreshDatabase;
use DishesTestTrait;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_user_can_generate_schedule(): void
{
$planner = $this->planner;

View file

@ -0,0 +1,135 @@
<?php
namespace Tests\Unit\Schedule\Actions;
use App\Models\Dish;
use App\Models\Schedule;
use App\Models\ScheduledUserDish;
use App\Models\User;
use Carbon\Carbon;
use DishPlanner\Schedule\Actions\GenerateScheduleForMonthAction;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use Tests\Traits\HasPlanner;
class GenerateScheduleForMonthActionTest extends TestCase
{
use HasPlanner;
use RefreshDatabase;
private GenerateScheduleForMonthAction $action;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
$this->action = new GenerateScheduleForMonthAction();
}
public function test_generates_schedule_for_entire_month(): void
{
$planner = $this->planner;
$user = User::factory()->planner($planner)->create();
$dish = Dish::factory()->planner($planner)->create();
$dish->users()->attach($user);
$month = 1;
$year = 2026;
$daysInMonth = Carbon::createFromDate($year, $month, 1)->daysInMonth;
$this->action->execute($planner, $month, $year, [$user->id]);
$this->assertDatabaseCount(Schedule::class, $daysInMonth);
$this->assertDatabaseCount(ScheduledUserDish::class, $daysInMonth);
}
public function test_generates_schedule_for_multiple_users(): void
{
$planner = $this->planner;
$users = User::factory()->planner($planner)->count(3)->create();
$dish = Dish::factory()->planner($planner)->create();
$dish->users()->attach($users);
$month = 2;
$year = 2026;
$daysInMonth = Carbon::createFromDate($year, $month, 1)->daysInMonth;
$this->action->execute($planner, $month, $year, $users->pluck('id')->toArray());
$this->assertDatabaseCount(Schedule::class, $daysInMonth);
$this->assertDatabaseCount(ScheduledUserDish::class, $daysInMonth * 3);
}
public function test_clears_existing_schedules_when_flag_is_true(): void
{
$planner = $this->planner;
$user = User::factory()->planner($planner)->create();
$dish = Dish::factory()->planner($planner)->create();
$dish->users()->attach($user);
$month = 3;
$year = 2026;
$this->action->execute($planner, $month, $year, [$user->id]);
$firstRunDishId = ScheduledUserDish::first()->user_dish_id;
$this->action->execute($planner, $month, $year, [$user->id], true);
$daysInMonth = Carbon::createFromDate($year, $month, 1)->daysInMonth;
$this->assertDatabaseCount(ScheduledUserDish::class, $daysInMonth);
}
public function test_preserves_existing_schedules_when_flag_is_false(): void
{
$planner = $this->planner;
$user = User::factory()->planner($planner)->create();
$dish = Dish::factory()->planner($planner)->create();
$dish->users()->attach($user);
$month = 4;
$year = 2026;
$this->action->execute($planner, $month, $year, [$user->id]);
$originalCount = ScheduledUserDish::count();
$this->action->execute($planner, $month, $year, [$user->id], false);
$this->assertDatabaseCount(ScheduledUserDish::class, $originalCount);
}
public function test_skips_users_without_dishes(): void
{
$planner = $this->planner;
$userWithDish = User::factory()->planner($planner)->create();
$userWithoutDish = User::factory()->planner($planner)->create();
$dish = Dish::factory()->planner($planner)->create();
$dish->users()->attach($userWithDish);
$month = 5;
$year = 2026;
$daysInMonth = Carbon::createFromDate($year, $month, 1)->daysInMonth;
$this->action->execute($planner, $month, $year, [$userWithDish->id, $userWithoutDish->id]);
$this->assertDatabaseCount(ScheduledUserDish::class, $daysInMonth);
$this->assertDatabaseMissing(ScheduledUserDish::class, ['user_id' => $userWithoutDish->id]);
}
public function test_only_generates_for_specified_users(): void
{
$planner = $this->planner;
$user1 = User::factory()->planner($planner)->create();
$user2 = User::factory()->planner($planner)->create();
$dish = Dish::factory()->planner($planner)->create();
$dish->users()->attach([$user1->id, $user2->id]);
$month = 6;
$year = 2026;
$daysInMonth = Carbon::createFromDate($year, $month, 1)->daysInMonth;
$this->action->execute($planner, $month, $year, [$user1->id]);
$this->assertDatabaseCount(ScheduledUserDish::class, $daysInMonth);
$this->assertDatabaseMissing(ScheduledUserDish::class, ['user_id' => $user2->id]);
}
}

View file

@ -0,0 +1,127 @@
<?php
namespace Tests\Unit\Schedule\Actions;
use App\Models\Dish;
use App\Models\Schedule;
use App\Models\ScheduledUserDish;
use App\Models\User;
use Carbon\Carbon;
use DishPlanner\Schedule\Actions\RegenerateScheduleForDateForUsersAction;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use Tests\Traits\HasPlanner;
class RegenerateScheduleForDateForUsersActionTest extends TestCase
{
use HasPlanner;
use RefreshDatabase;
private RegenerateScheduleForDateForUsersAction $action;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
$this->action = new RegenerateScheduleForDateForUsersAction();
}
public function test_regenerates_schedule_for_single_date(): void
{
$planner = $this->planner;
$user = User::factory()->planner($planner)->create();
$dish = Dish::factory()->planner($planner)->create();
$dish->users()->attach($user);
$date = Carbon::parse('2026-01-15');
$this->action->execute($planner, $date, [$user->id]);
$this->assertDatabaseCount(Schedule::class, 1);
$this->assertDatabaseCount(ScheduledUserDish::class, 1);
$this->assertDatabaseHas(Schedule::class, [
'planner_id' => $planner->id,
'date' => '2026-01-15',
]);
}
public function test_deletes_and_recreates_existing_schedule(): void
{
$planner = $this->planner;
$user = User::factory()->planner($planner)->create();
$dish1 = Dish::factory()->planner($planner)->create();
$dish2 = Dish::factory()->planner($planner)->create();
$dish1->users()->attach($user);
$dish2->users()->attach($user);
$date = Carbon::parse('2026-02-10');
$schedule = Schedule::create([
'planner_id' => $planner->id,
'date' => $date->format('Y-m-d'),
'is_skipped' => false,
]);
$originalUserDish = $user->userDishes->first();
ScheduledUserDish::create([
'schedule_id' => $schedule->id,
'user_id' => $user->id,
'user_dish_id' => $originalUserDish->id,
'is_skipped' => false,
]);
$this->action->execute($planner, $date, [$user->id]);
$this->assertDatabaseCount(ScheduledUserDish::class, 1);
}
public function test_regenerates_for_multiple_users(): void
{
$planner = $this->planner;
$users = User::factory()->planner($planner)->count(3)->create();
$dish = Dish::factory()->planner($planner)->create();
$dish->users()->attach($users);
$date = Carbon::parse('2026-03-20');
$this->action->execute($planner, $date, $users->pluck('id')->toArray());
$this->assertDatabaseCount(ScheduledUserDish::class, 3);
}
public function test_creates_schedule_if_not_exists(): void
{
$planner = $this->planner;
$user = User::factory()->planner($planner)->create();
$dish = Dish::factory()->planner($planner)->create();
$dish->users()->attach($user);
$date = Carbon::parse('2026-04-25');
$this->assertDatabaseCount(Schedule::class, 0);
$this->action->execute($planner, $date, [$user->id]);
$this->assertDatabaseCount(Schedule::class, 1);
$this->assertDatabaseHas(Schedule::class, [
'planner_id' => $planner->id,
'date' => '2026-04-25',
]);
}
public function test_skips_users_without_dishes(): void
{
$planner = $this->planner;
$userWithDish = User::factory()->planner($planner)->create();
$userWithoutDish = User::factory()->planner($planner)->create();
$dish = Dish::factory()->planner($planner)->create();
$dish->users()->attach($userWithDish);
$date = Carbon::parse('2026-05-05');
$this->action->execute($planner, $date, [$userWithDish->id, $userWithoutDish->id]);
$this->assertDatabaseCount(ScheduledUserDish::class, 1);
$this->assertDatabaseMissing(ScheduledUserDish::class, ['user_id' => $userWithoutDish->id]);
}
}

View file

@ -22,6 +22,12 @@ class ScheduleGeneratorTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_it_fills_up_the_next_2_weeks(): void
{
$planner = $this->planner;

View file

@ -0,0 +1,186 @@
<?php
namespace Tests\Unit\Schedule\Services;
use App\Models\Dish;
use App\Models\Schedule;
use App\Models\ScheduledUserDish;
use App\Models\User;
use Carbon\Carbon;
use DishPlanner\Schedule\Services\ScheduleCalendarService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use Tests\Traits\HasPlanner;
class ScheduleCalendarServiceTest extends TestCase
{
use HasPlanner;
use RefreshDatabase;
private ScheduleCalendarService $service;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
$this->service = new ScheduleCalendarService();
}
public function test_returns_31_calendar_days(): void
{
$planner = $this->planner;
$calendarDays = $this->service->getCalendarDays($planner, 1, 2026);
$this->assertCount(31, $calendarDays);
}
public function test_includes_correct_day_numbers(): void
{
$planner = $this->planner;
$month = 2;
$year = 2026;
$daysInMonth = Carbon::createFromDate($year, $month, 1)->daysInMonth;
$calendarDays = $this->service->getCalendarDays($planner, $month, $year);
for ($i = 0; $i < $daysInMonth; $i++) {
$this->assertEquals($i + 1, $calendarDays[$i]['day']);
}
for ($i = $daysInMonth; $i < 31; $i++) {
$this->assertNull($calendarDays[$i]['day']);
}
}
public function test_marks_today_correctly(): void
{
$planner = $this->planner;
$today = now();
$calendarDays = $this->service->getCalendarDays($planner, $today->month, $today->year);
$todayIndex = $today->day - 1;
$this->assertTrue($calendarDays[$todayIndex]['isToday']);
foreach ($calendarDays as $index => $day) {
if ($index !== $todayIndex && $day['day'] !== null) {
$this->assertFalse($day['isToday']);
}
}
}
public function test_includes_scheduled_dishes(): void
{
$planner = $this->planner;
$user = User::factory()->planner($planner)->create();
$dish = Dish::factory()->planner($planner)->create();
$dish->users()->attach($user);
$date = Carbon::createFromDate(2026, 3, 15);
$schedule = Schedule::create([
'planner_id' => $planner->id,
'date' => $date->format('Y-m-d'),
'is_skipped' => false,
]);
$userDish = $user->userDishes->first();
ScheduledUserDish::create([
'schedule_id' => $schedule->id,
'user_id' => $user->id,
'user_dish_id' => $userDish->id,
'is_skipped' => false,
]);
$calendarDays = $this->service->getCalendarDays($planner, 3, 2026);
$this->assertFalse($calendarDays[14]['isEmpty']);
$this->assertCount(1, $calendarDays[14]['scheduledDishes']);
}
public function test_empty_days_have_empty_scheduled_dishes(): void
{
$planner = $this->planner;
$calendarDays = $this->service->getCalendarDays($planner, 4, 2026);
foreach ($calendarDays as $day) {
if ($day['day'] !== null) {
$this->assertTrue($day['isEmpty']);
$this->assertCount(0, $day['scheduledDishes']);
}
}
}
public function test_only_loads_schedules_for_specified_planner(): void
{
$planner1 = $this->planner;
$planner2 = $this->createPlanner();
$user1 = User::factory()->planner($planner1)->create();
$user2 = User::factory()->planner($planner2)->create();
$dish1 = Dish::factory()->planner($planner1)->create();
$dish2 = Dish::factory()->planner($planner2)->create();
$dish1->users()->attach($user1);
$dish2->users()->attach($user2);
$date = Carbon::createFromDate(2026, 5, 10);
$schedule1 = Schedule::create([
'planner_id' => $planner1->id,
'date' => $date->format('Y-m-d'),
'is_skipped' => false,
]);
$schedule2 = Schedule::create([
'planner_id' => $planner2->id,
'date' => $date->format('Y-m-d'),
'is_skipped' => false,
]);
ScheduledUserDish::create([
'schedule_id' => $schedule1->id,
'user_id' => $user1->id,
'user_dish_id' => $user1->userDishes->first()->id,
'is_skipped' => false,
]);
ScheduledUserDish::create([
'schedule_id' => $schedule2->id,
'user_id' => $user2->id,
'user_dish_id' => $user2->userDishes->first()->id,
'is_skipped' => false,
]);
$calendarDays = $this->service->getCalendarDays($planner1, 5, 2026);
$this->assertCount(1, $calendarDays[9]['scheduledDishes']);
$this->assertEquals($user1->id, $calendarDays[9]['scheduledDishes']->first()->user_id);
}
public function test_get_month_name_returns_correct_format(): void
{
$this->assertEquals('January 2026', $this->service->getMonthName(1, 2026));
$this->assertEquals('December 2025', $this->service->getMonthName(12, 2025));
$this->assertEquals('February 2027', $this->service->getMonthName(2, 2027));
}
public function test_handles_february_in_leap_year(): void
{
$planner = $this->planner;
$calendarDays = $this->service->getCalendarDays($planner, 2, 2028);
$this->assertCount(31, $calendarDays);
for ($i = 0; $i < 29; $i++) {
$this->assertNotNull($calendarDays[$i]['day']);
}
for ($i = 29; $i < 31; $i++) {
$this->assertNull($calendarDays[$i]['day']);
}
}
}

View file

@ -14,6 +14,12 @@ class ScheduleRepositoryTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_find_or_create_finds_existing_model(): void
{
$planner = $this->planner;

View file

@ -0,0 +1,152 @@
<?php
namespace Tests\Unit\ScheduledUserDish\Actions;
use App\Models\Dish;
use App\Models\Schedule;
use App\Models\ScheduledUserDish;
use App\Models\User;
use Carbon\Carbon;
use DishPlanner\ScheduledUserDish\Actions\DeleteScheduledUserDishForDateAction;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use Tests\Traits\HasPlanner;
class DeleteScheduledUserDishForDateActionTest extends TestCase
{
use HasPlanner;
use RefreshDatabase;
private DeleteScheduledUserDishForDateAction $action;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
$this->action = new DeleteScheduledUserDishForDateAction();
}
public function test_deletes_scheduled_user_dish(): void
{
$planner = $this->planner;
$user = User::factory()->planner($planner)->create();
$dish = Dish::factory()->planner($planner)->create();
$dish->users()->attach($user);
$date = Carbon::parse('2026-01-15');
$schedule = Schedule::create([
'planner_id' => $planner->id,
'date' => $date->format('Y-m-d'),
'is_skipped' => false,
]);
$userDish = $user->userDishes->first();
ScheduledUserDish::create([
'schedule_id' => $schedule->id,
'user_id' => $user->id,
'user_dish_id' => $userDish->id,
'is_skipped' => false,
]);
$result = $this->action->execute($planner, $date, $user->id);
$this->assertTrue($result);
$this->assertDatabaseCount(ScheduledUserDish::class, 0);
}
public function test_returns_false_when_schedule_does_not_exist(): void
{
$planner = $this->planner;
$user = User::factory()->planner($planner)->create();
$date = Carbon::parse('2026-02-20');
$result = $this->action->execute($planner, $date, $user->id);
$this->assertFalse($result);
}
public function test_returns_false_when_scheduled_user_dish_does_not_exist(): void
{
$planner = $this->planner;
$user = User::factory()->planner($planner)->create();
$date = Carbon::parse('2026-03-10');
Schedule::create([
'planner_id' => $planner->id,
'date' => $date->format('Y-m-d'),
'is_skipped' => false,
]);
$result = $this->action->execute($planner, $date, $user->id);
$this->assertFalse($result);
}
public function test_only_deletes_for_specified_user(): void
{
$planner = $this->planner;
$user1 = User::factory()->planner($planner)->create();
$user2 = User::factory()->planner($planner)->create();
$dish = Dish::factory()->planner($planner)->create();
$dish->users()->attach([$user1->id, $user2->id]);
$date = Carbon::parse('2026-04-05');
$schedule = Schedule::create([
'planner_id' => $planner->id,
'date' => $date->format('Y-m-d'),
'is_skipped' => false,
]);
$user1Dish = $user1->userDishes->first();
$user2Dish = $user2->userDishes->first();
ScheduledUserDish::create([
'schedule_id' => $schedule->id,
'user_id' => $user1->id,
'user_dish_id' => $user1Dish->id,
'is_skipped' => false,
]);
ScheduledUserDish::create([
'schedule_id' => $schedule->id,
'user_id' => $user2->id,
'user_dish_id' => $user2Dish->id,
'is_skipped' => false,
]);
$this->action->execute($planner, $date, $user1->id);
$this->assertDatabaseCount(ScheduledUserDish::class, 1);
$this->assertDatabaseMissing(ScheduledUserDish::class, ['user_id' => $user1->id]);
$this->assertDatabaseHas(ScheduledUserDish::class, ['user_id' => $user2->id]);
}
public function test_preserves_schedule_after_deletion(): void
{
$planner = $this->planner;
$user = User::factory()->planner($planner)->create();
$dish = Dish::factory()->planner($planner)->create();
$dish->users()->attach($user);
$date = Carbon::parse('2026-05-15');
$schedule = Schedule::create([
'planner_id' => $planner->id,
'date' => $date->format('Y-m-d'),
'is_skipped' => false,
]);
$userDish = $user->userDishes->first();
ScheduledUserDish::create([
'schedule_id' => $schedule->id,
'user_id' => $user->id,
'user_dish_id' => $userDish->id,
'is_skipped' => false,
]);
$this->action->execute($planner, $date, $user->id);
$this->assertDatabaseCount(Schedule::class, 1);
$this->assertDatabaseHas(Schedule::class, ['id' => $schedule->id]);
}
}

View file

@ -0,0 +1,135 @@
<?php
namespace Tests\Unit\ScheduledUserDish\Actions;
use App\Models\Dish;
use App\Models\Schedule;
use App\Models\ScheduledUserDish;
use App\Models\User;
use Carbon\Carbon;
use DishPlanner\ScheduledUserDish\Actions\SkipScheduledUserDishForDateAction;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
use Tests\Traits\HasPlanner;
class SkipScheduledUserDishForDateActionTest extends TestCase
{
use HasPlanner;
use RefreshDatabase;
private SkipScheduledUserDishForDateAction $action;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
$this->action = new SkipScheduledUserDishForDateAction();
}
public function test_skips_scheduled_user_dish(): void
{
$planner = $this->planner;
$user = User::factory()->planner($planner)->create();
$dish = Dish::factory()->planner($planner)->create();
$dish->users()->attach($user);
$date = Carbon::parse('2026-01-15');
$schedule = Schedule::create([
'planner_id' => $planner->id,
'date' => $date->format('Y-m-d'),
'is_skipped' => false,
]);
$userDish = $user->userDishes->first();
ScheduledUserDish::create([
'schedule_id' => $schedule->id,
'user_id' => $user->id,
'user_dish_id' => $userDish->id,
'is_skipped' => false,
]);
$result = $this->action->execute($planner, $date, $user->id);
$this->assertTrue($result);
$this->assertDatabaseHas(ScheduledUserDish::class, [
'schedule_id' => $schedule->id,
'user_id' => $user->id,
'is_skipped' => true,
'user_dish_id' => null,
]);
}
public function test_returns_false_when_schedule_does_not_exist(): void
{
$planner = $this->planner;
$user = User::factory()->planner($planner)->create();
$date = Carbon::parse('2026-02-20');
$result = $this->action->execute($planner, $date, $user->id);
$this->assertFalse($result);
}
public function test_returns_false_when_scheduled_user_dish_does_not_exist(): void
{
$planner = $this->planner;
$user = User::factory()->planner($planner)->create();
$date = Carbon::parse('2026-03-10');
Schedule::create([
'planner_id' => $planner->id,
'date' => $date->format('Y-m-d'),
'is_skipped' => false,
]);
$result = $this->action->execute($planner, $date, $user->id);
$this->assertFalse($result);
}
public function test_only_skips_for_specified_user(): void
{
$planner = $this->planner;
$user1 = User::factory()->planner($planner)->create();
$user2 = User::factory()->planner($planner)->create();
$dish = Dish::factory()->planner($planner)->create();
$dish->users()->attach([$user1->id, $user2->id]);
$date = Carbon::parse('2026-04-05');
$schedule = Schedule::create([
'planner_id' => $planner->id,
'date' => $date->format('Y-m-d'),
'is_skipped' => false,
]);
$user1Dish = $user1->userDishes->first();
$user2Dish = $user2->userDishes->first();
ScheduledUserDish::create([
'schedule_id' => $schedule->id,
'user_id' => $user1->id,
'user_dish_id' => $user1Dish->id,
'is_skipped' => false,
]);
ScheduledUserDish::create([
'schedule_id' => $schedule->id,
'user_id' => $user2->id,
'user_dish_id' => $user2Dish->id,
'is_skipped' => false,
]);
$this->action->execute($planner, $date, $user1->id);
$this->assertDatabaseHas(ScheduledUserDish::class, [
'user_id' => $user1->id,
'is_skipped' => true,
]);
$this->assertDatabaseHas(ScheduledUserDish::class, [
'user_id' => $user2->id,
'is_skipped' => false,
]);
}
}

View file

@ -19,6 +19,12 @@ class UserDishRepositoryTest extends TestCase
use HasPlanner;
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->setUpHasPlanner();
}
public function test_find_interfering_dishes(): void
{
$planner = $this->planner;