diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4ee7c7f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +.git +.github +node_modules +vendor +.env +.env.* +!.env.example +.idea +.vscode +storage/logs/* +storage/framework/cache/* +storage/framework/sessions/* +storage/framework/views/* +storage/app/screenshots/* +bootstrap/cache/* +*.log +.DS_Store +Thumbs.db +.claude +*.sqlite diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5ff33ff --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,188 @@ +services: + # Laravel Application + app: + build: + context: . + dockerfile: docker/app/Dockerfile + container_name: chat-app + restart: unless-stopped + extra_hosts: + - 'host.docker.internal:host-gateway' + ports: + - '${APP_PORT:-8000}:8000' + environment: + APP_ENV: '${APP_ENV:-local}' + APP_DEBUG: '${APP_DEBUG:-true}' + DB_CONNECTION: pgsql + DB_HOST: postgres + DB_PORT: 5432 + DB_DATABASE: '${DB_DATABASE:-chat}' + DB_USERNAME: '${DB_USERNAME:-chat}' + DB_PASSWORD: '${DB_PASSWORD:-secret}' + REDIS_HOST: redis + REDIS_PORT: 6379 + CACHE_STORE: redis + SESSION_DRIVER: redis + QUEUE_CONNECTION: redis + MAIL_MAILER: smtp + MAIL_HOST: mailpit + MAIL_PORT: 1025 + volumes: + - '.:/var/www/html' + - './docker/app/php.ini:/usr/local/etc/php/conf.d/custom.ini:ro' + networks: + - chat + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "php", "artisan", "about"] + interval: 10s + timeout: 5s + retries: 3 + + # Vite Dev Server + vite: + build: + context: . + dockerfile: docker/node/Dockerfile + container_name: chat-vite + restart: unless-stopped + ports: + - '${VITE_PORT:-5173}:5173' + volumes: + - '.:/var/www/html' + - '/var/www/html/node_modules' + networks: + - chat + command: npm run dev -- --host 0.0.0.0 + + # Queue Worker (Horizon) + horizon: + build: + context: . + dockerfile: docker/app/Dockerfile + container_name: chat-horizon + restart: unless-stopped + environment: + APP_ENV: '${APP_ENV:-local}' + DB_CONNECTION: pgsql + DB_HOST: postgres + DB_PORT: 5432 + DB_DATABASE: '${DB_DATABASE:-chat}' + DB_USERNAME: '${DB_USERNAME:-chat}' + DB_PASSWORD: '${DB_PASSWORD:-secret}' + REDIS_HOST: redis + REDIS_PORT: 6379 + QUEUE_CONNECTION: redis + volumes: + - '.:/var/www/html' + networks: + - chat + depends_on: + - app + - redis + command: php artisan horizon + + # Scheduler (Cron) + scheduler: + build: + context: . + dockerfile: docker/app/Dockerfile + container_name: chat-scheduler + restart: unless-stopped + environment: + APP_ENV: '${APP_ENV:-local}' + DB_CONNECTION: pgsql + DB_HOST: postgres + DB_PORT: 5432 + DB_DATABASE: '${DB_DATABASE:-chat}' + DB_USERNAME: '${DB_USERNAME:-chat}' + DB_PASSWORD: '${DB_PASSWORD:-secret}' + REDIS_HOST: redis + volumes: + - '.:/var/www/html' + networks: + - chat + depends_on: + - app + command: sh -c "while true; do php artisan schedule:run --verbose; sleep 60; done" + + # PostgreSQL with pgvector for AI embeddings + postgres: + image: pgvector/pgvector:pg16 + container_name: chat-postgres + restart: unless-stopped + ports: + - '${DB_PORT:-5432}:5432' + environment: + POSTGRES_DB: '${DB_DATABASE:-chat}' + POSTGRES_USER: '${DB_USERNAME:-chat}' + POSTGRES_PASSWORD: '${DB_PASSWORD:-secret}' + volumes: + - chat-postgres:/var/lib/postgresql/data + - ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql:ro + networks: + - chat + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-chat}"] + interval: 5s + timeout: 5s + retries: 5 + + # Redis + redis: + image: redis:7-alpine + container_name: chat-redis + restart: unless-stopped + ports: + - '${REDIS_PORT:-6379}:6379' + volumes: + - chat-redis:/data + networks: + - chat + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 5 + command: redis-server --appendonly yes + + # Mailpit (Email Testing) + mailpit: + image: axllent/mailpit:latest + container_name: chat-mailpit + restart: unless-stopped + ports: + - '${MAILPIT_PORT:-8025}:8025' + - '1025:1025' + networks: + - chat + + # Browser Testing (Playwright) + playwright: + build: + context: . + dockerfile: docker/playwright/Dockerfile + container_name: chat-playwright + volumes: + - '.:/var/www/html' + - './storage/app/screenshots:/var/www/html/storage/app/screenshots' + networks: + - chat + depends_on: + - app + profiles: + - testing + +networks: + chat: + driver: bridge + +volumes: + chat-postgres: + driver: local + chat-redis: + driver: local diff --git a/docker/app/Dockerfile b/docker/app/Dockerfile new file mode 100644 index 0000000..b258b22 --- /dev/null +++ b/docker/app/Dockerfile @@ -0,0 +1,52 @@ +FROM php:8.4-cli + +LABEL maintainer="Chat App" + +ARG WWWGROUP=1000 +ARG NODE_VERSION=22 + +WORKDIR /var/www/html + +ENV DEBIAN_FRONTEND=noninteractive +ENV TZ=UTC + +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + gnupg gosu curl ca-certificates zip unzip git supervisor \ + libpng-dev libonig-dev libxml2-dev libzip-dev libpq-dev \ + libcurl4-openssl-dev libssl-dev \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install PHP extensions +RUN docker-php-ext-install \ + pdo pdo_pgsql pgsql \ + mbstring exif pcntl bcmath gd zip \ + opcache sockets + +# Install Redis extension +RUN pecl install redis && docker-php-ext-enable redis + +# Install Composer +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +# Create sail user +RUN groupadd --force -g $WWWGROUP sail \ + && useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail + +# Copy application +COPY . /var/www/html + +# Install dependencies (if vendor doesn't exist) +RUN if [ ! -d "vendor" ]; then composer install --no-interaction --prefer-dist --optimize-autoloader; fi + +# Setup permissions +RUN chown -R sail:sail /var/www/html \ + && chmod -R 755 /var/www/html/storage /var/www/html/bootstrap/cache + +USER sail + +EXPOSE 8000 + +CMD ["php", "artisan", "serve", "--host=0.0.0.0", "--port=8000"] diff --git a/docker/app/php.ini b/docker/app/php.ini new file mode 100644 index 0000000..8ac4f73 --- /dev/null +++ b/docker/app/php.ini @@ -0,0 +1,17 @@ +[PHP] +memory_limit = 512M +upload_max_filesize = 100M +post_max_size = 100M +max_execution_time = 300 +max_input_time = 300 + +[opcache] +opcache.enable = 1 +opcache.memory_consumption = 256 +opcache.interned_strings_buffer = 16 +opcache.max_accelerated_files = 20000 +opcache.validate_timestamps = 1 +opcache.revalidate_freq = 0 + +[Date] +date.timezone = UTC diff --git a/docker/node/Dockerfile b/docker/node/Dockerfile new file mode 100644 index 0000000..e144ca9 --- /dev/null +++ b/docker/node/Dockerfile @@ -0,0 +1,19 @@ +FROM node:22-alpine + +WORKDIR /var/www/html + +# Install dependencies for native modules +RUN apk add --no-cache python3 make g++ + +# Copy package files +COPY package*.json ./ + +# Install dependencies +RUN npm ci + +# Copy application +COPY . . + +EXPOSE 5173 + +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"] diff --git a/docker/playwright/Dockerfile b/docker/playwright/Dockerfile new file mode 100644 index 0000000..7b051db --- /dev/null +++ b/docker/playwright/Dockerfile @@ -0,0 +1,28 @@ +FROM mcr.microsoft.com/playwright:v1.49.0-jammy + +WORKDIR /var/www/html + +# Install PHP 8.4 +RUN apt-get update && apt-get install -y \ + software-properties-common \ + && add-apt-repository ppa:ondrej/php \ + && apt-get update \ + && apt-get install -y \ + php8.4-cli php8.4-pgsql php8.4-redis php8.4-mbstring \ + php8.4-xml php8.4-curl php8.4-zip php8.4-gd php8.4-bcmath \ + composer \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Copy application +COPY . /var/www/html + +# Install PHP dependencies +RUN if [ ! -d "vendor" ]; then composer install --no-interaction; fi + +# Install Node dependencies +RUN npm ci + +# Build frontend +RUN npm run build + +CMD ["php", "artisan", "test", "--filter=Browser"] diff --git a/docker/postgres/init.sql b/docker/postgres/init.sql new file mode 100644 index 0000000..afb8be1 --- /dev/null +++ b/docker/postgres/init.sql @@ -0,0 +1,6 @@ +-- Enable pgvector extension for AI embeddings +CREATE EXTENSION IF NOT EXISTS vector; + +-- Enable other useful extensions +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; +CREATE EXTENSION IF NOT EXISTS "pg_trgm"; -- For fuzzy text search diff --git a/scripts/docker-browser-test.sh b/scripts/docker-browser-test.sh new file mode 100755 index 0000000..2c19100 --- /dev/null +++ b/scripts/docker-browser-test.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# Run browser tests with Playwright in Docker +set -e + +FILTER=${1:-"Browser"} + +echo "๐ŸŽญ Running browser tests with Playwright..." +echo " Filter: $FILTER" + +# Use the playwright profile +docker compose --profile testing run --rm playwright php artisan test --filter="$FILTER" + +echo "" +echo "๐Ÿ“ธ Screenshots saved to: storage/app/screenshots/" diff --git a/scripts/docker-fresh.sh b/scripts/docker-fresh.sh new file mode 100755 index 0000000..660d868 --- /dev/null +++ b/scripts/docker-fresh.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Fresh install: rebuild everything from scratch +set -e + +echo "๐Ÿ”„ Fresh Docker build..." + +# Stop and remove everything +docker compose down -v --remove-orphans + +# Rebuild +docker compose build --no-cache + +# Start +docker compose up -d + +# Run migrations and seed +docker compose exec app php artisan migrate:fresh --seed + +echo "" +echo "โœ… Fresh install complete!" diff --git a/scripts/docker-test.sh b/scripts/docker-test.sh new file mode 100755 index 0000000..d661ba9 --- /dev/null +++ b/scripts/docker-test.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# Run tests in Docker (including browser tests) +set -e + +FILTER=${1:-""} + +echo "๐Ÿงช Running tests in Docker..." + +if [ -n "$FILTER" ]; then + echo " Filter: $FILTER" + docker compose run --rm app php artisan test --filter="$FILTER" +else + docker compose run --rm app php artisan test +fi diff --git a/scripts/docker-up.sh b/scripts/docker-up.sh new file mode 100755 index 0000000..5798297 --- /dev/null +++ b/scripts/docker-up.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# Start the full Docker stack +set -e + +echo "๐Ÿณ Starting Chat Docker Stack..." + +# Build and start +docker compose up -d --build + +echo "" +echo "โœ… Stack is running!" +echo "" +echo "Services:" +echo " App: http://localhost:8000" +echo " Vite: http://localhost:5173" +echo " Mailpit: http://localhost:8025" +echo " Postgres: localhost:5432" +echo " Redis: localhost:6379" +echo "" +echo "Commands:" +echo " docker compose logs -f app # Watch app logs" +echo " docker compose exec app bash # Shell into app" +echo " ./scripts/docker-test.sh # Run browser tests" diff --git a/tests/Browser/UiReviewAgentTest.php b/tests/Browser/UiReviewAgentTest.php new file mode 100644 index 0000000..834172a --- /dev/null +++ b/tests/Browser/UiReviewAgentTest.php @@ -0,0 +1,512 @@ +create([ + 'two_factor_secret' => null, + 'two_factor_confirmed_at' => null, + ]); +} + +function createTestChat(User $user): Chat +{ + $model = AiModel::factory()->create(['is_available' => true]); + + return Chat::factory()->for($user)->create(['ai_model_id' => $model->id]); +} + +/* +|-------------------------------------------------------------------------- +| Public Pages Review +|-------------------------------------------------------------------------- +*/ + +describe('public pages', function () { + it('reviews welcome page', function () { + $page = visit('/'); + + $page->assertSuccessful() + ->assertNoJavaScriptErrors() + ->assertNoConsoleLogs() + ->screenshot('review/public/welcome-light'); + + $page->inDarkMode() + ->screenshot('review/public/welcome-dark'); + + $page->on()->mobile() + ->screenshot('review/public/welcome-mobile'); + }); + + it('reviews login page', function () { + $page = visit('/login'); + + $page->assertSuccessful() + ->assertSee('Log in') + ->assertNoJavaScriptErrors() + ->screenshot('review/public/login-light'); + + $page->inDarkMode() + ->screenshot('review/public/login-dark'); + + $page->on()->mobile() + ->screenshot('review/public/login-mobile'); + }); + + it('reviews registration page', function () { + $page = visit('/register'); + + $page->assertSuccessful() + ->assertSee('Create an account') + ->assertNoJavaScriptErrors() + ->screenshot('review/public/register-light'); + + $page->inDarkMode() + ->screenshot('review/public/register-dark'); + + $page->on()->mobile() + ->screenshot('review/public/register-mobile'); + }); +}); + +/* +|-------------------------------------------------------------------------- +| Dashboard Review +|-------------------------------------------------------------------------- +*/ + +describe('dashboard', function () { + it('reviews dashboard page', function () { + $user = createTestUser(); + $this->actingAs($user); + + $page = visit('/dashboard'); + + $page->assertSuccessful() + ->assertSee('Dashboard') + ->assertNoJavaScriptErrors() + ->screenshot('review/dashboard/main-light'); + + $page->inDarkMode() + ->screenshot('review/dashboard/main-dark'); + + $page->on()->mobile() + ->screenshot('review/dashboard/main-mobile'); + }); + + it('reviews dashboard with recent chats', function () { + $user = createTestUser(); + $model = AiModel::factory()->create(['is_available' => true]); + + // Create some chats + Chat::factory()->count(3)->for($user)->create(['ai_model_id' => $model->id]); + + $this->actingAs($user); + + $page = visit('/dashboard'); + + $page->assertSuccessful() + ->assertNoJavaScriptErrors() + ->screenshot('review/dashboard/with-chats'); + }); +}); + +/* +|-------------------------------------------------------------------------- +| Chat Pages Review +|-------------------------------------------------------------------------- +*/ + +describe('chat pages', function () { + it('reviews chat index page', function () { + $user = createTestUser(); + $this->actingAs($user); + + $page = visit('/chats'); + + $page->assertSuccessful() + ->assertNoJavaScriptErrors() + ->screenshot('review/chats/index-empty-light'); + + $page->inDarkMode() + ->screenshot('review/chats/index-empty-dark'); + }); + + it('reviews chat index with chats', function () { + $user = createTestUser(); + $model = AiModel::factory()->create(['is_available' => true]); + Chat::factory()->count(5)->for($user)->create(['ai_model_id' => $model->id]); + + $this->actingAs($user); + + $page = visit('/chats'); + + $page->assertSuccessful() + ->assertNoJavaScriptErrors() + ->screenshot('review/chats/index-with-chats'); + }); + + it('reviews individual chat page', function () { + $user = createTestUser(); + $chat = createTestChat($user); + + $this->actingAs($user); + + $page = visit('/chats/'.$chat->id); + + $page->assertSuccessful() + ->assertSee($chat->title) + ->assertNoJavaScriptErrors() + ->assertVisible('@message-input') + ->screenshot('review/chats/show-empty-light'); + + $page->inDarkMode() + ->screenshot('review/chats/show-empty-dark'); + + $page->on()->mobile() + ->screenshot('review/chats/show-empty-mobile'); + }); + + it('reviews chat page with messages', function () { + $user = createTestUser(); + $chat = createTestChat($user); + + // Add some messages using factory states + Message::factory()->for($chat)->user()->create(['parts' => ['text' => 'Hello, how are you?']]); + Message::factory()->for($chat)->assistant()->create(['parts' => ['text' => 'I am doing well, thank you for asking! How can I help you today?']]); + Message::factory()->for($chat)->user()->create(['parts' => ['text' => 'Can you explain how Laravel works?']]); + Message::factory()->for($chat)->assistant()->create(['parts' => ['text' => 'Laravel is a PHP web application framework with expressive, elegant syntax. It provides tools for routing, authentication, sessions, caching, and more.']]); + + $this->actingAs($user); + + $page = visit('/chats/'.$chat->id); + + $page->assertSuccessful() + ->assertNoJavaScriptErrors() + ->screenshot('review/chats/show-with-messages-light'); + + $page->inDarkMode() + ->screenshot('review/chats/show-with-messages-dark'); + + $page->on()->mobile() + ->screenshot('review/chats/show-with-messages-mobile'); + }); +}); + +/* +|-------------------------------------------------------------------------- +| Settings Pages Review +|-------------------------------------------------------------------------- +*/ + +describe('settings pages', function () { + it('reviews profile settings', function () { + $user = createTestUser(); + $this->actingAs($user); + + $page = visit('/settings/profile'); + + $page->assertSuccessful() + ->assertSee('Profile') + ->assertNoJavaScriptErrors() + ->screenshot('review/settings/profile-light'); + + $page->inDarkMode() + ->screenshot('review/settings/profile-dark'); + + $page->on()->mobile() + ->screenshot('review/settings/profile-mobile'); + }); + + it('reviews password settings', function () { + $user = createTestUser(); + $this->actingAs($user); + + $page = visit('/settings/password'); + + $page->assertSuccessful() + ->assertNoJavaScriptErrors() + ->screenshot('review/settings/password-light'); + + $page->inDarkMode() + ->screenshot('review/settings/password-dark'); + }); + + it('reviews appearance settings', function () { + $user = createTestUser(); + $this->actingAs($user); + + $page = visit('/settings/appearance'); + + $page->assertSuccessful() + ->assertNoJavaScriptErrors() + ->screenshot('review/settings/appearance-light'); + + $page->inDarkMode() + ->screenshot('review/settings/appearance-dark'); + }); + + it('reviews two-factor settings', function () { + $user = createTestUser(); + $this->actingAs($user); + + $page = visit('/settings/two-factor'); + + $page->assertSuccessful() + ->assertNoJavaScriptErrors() + ->screenshot('review/settings/two-factor-light'); + + $page->inDarkMode() + ->screenshot('review/settings/two-factor-dark'); + }); + + it('reviews provider settings - empty state', function () { + $user = createTestUser(); + $this->actingAs($user); + + $page = visit('/settings/providers'); + + $page->assertSuccessful() + ->assertSee('No providers configured yet') + ->assertNoJavaScriptErrors() + ->screenshot('review/settings/providers-empty-light'); + + $page->inDarkMode() + ->screenshot('review/settings/providers-empty-dark'); + }); + + it('reviews provider settings - with providers', function () { + $user = createTestUser(); + UserApiCredential::factory()->for($user)->create(['provider' => 'openai']); + UserApiCredential::factory()->for($user)->create(['provider' => 'anthropic']); + + $this->actingAs($user); + + $page = visit('/settings/providers'); + + $page->assertSuccessful() + ->assertNoJavaScriptErrors() + ->screenshot('review/settings/providers-with-data-light'); + + $page->inDarkMode() + ->screenshot('review/settings/providers-with-data-dark'); + }); +}); + +/* +|-------------------------------------------------------------------------- +| Navigation Flow Review +|-------------------------------------------------------------------------- +*/ + +describe('navigation flows', function () { + it('reviews settings navigation flow', function () { + $user = createTestUser(); + $this->actingAs($user); + + $page = visit('/settings/profile'); + + // Navigate through all settings tabs + $page->assertSee('Profile') + ->screenshot('review/navigation/settings-1-profile'); + + $page->click('Password') + ->assertUrlContains('/settings/password') + ->screenshot('review/navigation/settings-2-password'); + + $page->click('Appearance') + ->assertUrlContains('/settings/appearance') + ->screenshot('review/navigation/settings-3-appearance'); + + $page->click('Providers') + ->assertUrlContains('/settings/providers') + ->screenshot('review/navigation/settings-4-providers'); + + $page->assertNoJavaScriptErrors(); + }); + + it('reviews dashboard to chat flow', function () { + $user = createTestUser(); + $chat = createTestChat($user); + + $this->actingAs($user); + + $page = visit('/dashboard'); + + $page->assertSee('Dashboard') + ->screenshot('review/navigation/flow-1-dashboard'); + + $page->click('Chats') + ->assertUrlContains('/chats') + ->screenshot('review/navigation/flow-2-chats'); + + $page->assertNoJavaScriptErrors(); + }); +}); + +/* +|-------------------------------------------------------------------------- +| Responsive Breakpoints Review +|-------------------------------------------------------------------------- +*/ + +describe('responsive breakpoints', function () { + it('reviews chat page at all breakpoints', function () { + $user = createTestUser(); + $chat = createTestChat($user); + + $this->actingAs($user); + + $page = visit('/chats/'.$chat->id); + + // Desktop (1920px) + $page->resize(1920, 1080) + ->screenshot('review/responsive/chat-1920'); + + // Laptop (1366px) + $page->resize(1366, 768) + ->screenshot('review/responsive/chat-1366'); + + // Tablet landscape (1024px) + $page->resize(1024, 768) + ->screenshot('review/responsive/chat-1024'); + + // Tablet portrait (768px) + $page->resize(768, 1024) + ->screenshot('review/responsive/chat-768'); + + // Mobile large (425px) + $page->resize(425, 896) + ->screenshot('review/responsive/chat-425'); + + // Mobile medium (375px) + $page->resize(375, 812) + ->screenshot('review/responsive/chat-375'); + + // Mobile small (320px) + $page->resize(320, 568) + ->screenshot('review/responsive/chat-320'); + + $page->assertNoJavaScriptErrors(); + }); + + it('reviews dashboard at tablet breakpoint', function () { + $user = createTestUser(); + $this->actingAs($user); + + $page = visit('/dashboard'); + + $page->resize(768, 1024) + ->assertNoJavaScriptErrors() + ->screenshot('review/responsive/dashboard-tablet'); + }); +}); + +/* +|-------------------------------------------------------------------------- +| Accessibility Basics Review +|-------------------------------------------------------------------------- +*/ + +describe('accessibility basics', function () { + it('verifies login form has proper labels', function () { + $page = visit('/login'); + + // Check form elements have associated labels + $page->assertScript('document.querySelector("label[for=\'email\']") !== null', true) + ->assertScript('document.querySelector("label[for=\'password\']") !== null', true) + ->assertNoJavaScriptErrors(); + }); + + it('verifies chat input is keyboard accessible', function () { + $user = createTestUser(); + $chat = createTestChat($user); + + $this->actingAs($user); + + $page = visit('/chats/'.$chat->id); + + // Tab to the message input and verify it's focusable + $page->assertVisible('@message-input') + ->click('@message-input') + ->assertNoJavaScriptErrors(); + }); + + it('verifies page has proper heading structure', function () { + $user = createTestUser(); + $this->actingAs($user); + + $page = visit('/dashboard'); + + // Check that there's at least one h1 + $page->assertScript('document.querySelector("h1") !== null', true) + ->assertNoJavaScriptErrors(); + }); +}); + +/* +|-------------------------------------------------------------------------- +| Error State Review +|-------------------------------------------------------------------------- +*/ + +describe('error states', function () { + it('reviews 404 page', function () { + $user = createTestUser(); + $this->actingAs($user); + + $page = visit('/this-page-does-not-exist-12345'); + + $page->screenshot('review/errors/404-light'); + + $page->inDarkMode() + ->screenshot('review/errors/404-dark'); + }); +}); + +/* +|-------------------------------------------------------------------------- +| Performance Check +|-------------------------------------------------------------------------- +*/ + +describe('performance', function () { + it('verifies pages load within acceptable time', function () { + $user = createTestUser(); + $this->actingAs($user); + + $startTime = microtime(true); + $page = visit('/dashboard'); + $loadTime = microtime(true) - $startTime; + + $page->assertNoJavaScriptErrors(); + + // Page should load in under 5 seconds + expect($loadTime)->toBeLessThan(5.0); + }); +});