From fc814c221553df5dcc0f224567937c534c54024f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 13 Dec 2025 05:47:34 +0000 Subject: [PATCH 1/3] feat: add Docker environment for browser testing - Dockerfile for production builds - Dockerfile.dev for development with volume mounts - docker-compose.yml for production - docker-compose.dev.yml for development - docker-entrypoint.sh for container initialization - scripts/docker-test.sh for running browser tests in Docker - .dockerignore to keep images clean Includes Playwright/Chromium deps for Pest 4 browser testing. --- .dockerignore | 14 +++++++ Dockerfile | 88 ++++++++++++++++++++++++++++++++++++++++++ Dockerfile.dev | 41 ++++++++++++++++++++ docker-compose.dev.yml | 27 +++++++++++++ docker-compose.yml | 37 ++++++++++++++++++ docker-entrypoint.sh | 23 +++++++++++ scripts/docker-test.sh | 29 ++++++++++++++ 7 files changed, 259 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 Dockerfile.dev create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.yml create mode 100644 docker-entrypoint.sh create mode 100755 scripts/docker-test.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c85a817 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +/.git +/node_modules +/vendor +/.env +/.idea +/.vscode +/storage/logs/* +/storage/framework/cache/* +/storage/framework/sessions/* +/storage/framework/views/* +/bootstrap/cache/* +*.log +.DS_Store +Thumbs.db diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..70b39db --- /dev/null +++ b/Dockerfile @@ -0,0 +1,88 @@ +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 \ + sqlite3 \ + libcap2-bin \ + libpng-dev \ + libonig-dev \ + libxml2-dev \ + libzip-dev \ + libsqlite3-dev \ + libnss3 \ + libatk1.0-0 \ + libatk-bridge2.0-0 \ + libcups2 \ + libdrm2 \ + libxkbcommon0 \ + libxcomposite1 \ + libxdamage1 \ + libxfixes3 \ + libxrandr2 \ + libgbm1 \ + libasound2 \ + libpango-1.0-0 \ + libcairo2 \ + libatspi2.0-0 \ + libgtk-3-0 \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Install PHP extensions +RUN docker-php-ext-install pdo pdo_sqlite mbstring exif pcntl bcmath gd zip + +# Install Composer +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +# Install Node.js +RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - \ + && apt-get install -y nodejs \ + && npm install -g npm + +# 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 +RUN composer install --no-interaction --prefer-dist --optimize-autoloader +RUN npm ci + +# Install Playwright browsers +RUN npx playwright install chromium --with-deps + +# Build frontend +RUN npm run build + +# Setup permissions +RUN chown -R sail:sail /var/www/html \ + && chmod -R 755 /var/www/html/storage + +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + +EXPOSE 80 5173 + +ENTRYPOINT ["docker-entrypoint.sh"] diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..18f4f5a --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,41 @@ +FROM php:8.4-cli + +LABEL maintainer="Chat App Dev" + +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 (including Playwright requirements) +RUN apt-get update && apt-get install -y \ + gnupg gosu curl ca-certificates zip unzip git sqlite3 \ + libpng-dev libonig-dev libxml2-dev libzip-dev libsqlite3-dev \ + # Playwright/Chromium dependencies + libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \ + libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \ + libgbm1 libasound2 libpango-1.0-0 libcairo2 libatspi2.0-0 \ + libgtk-3-0 fonts-liberation libappindicator3-1 xdg-utils \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install PHP extensions +RUN docker-php-ext-install pdo pdo_sqlite mbstring exif pcntl bcmath gd zip + +# Install Composer +COPY --from=composer:latest /usr/bin/composer /usr/bin/composer + +# Install Node.js +RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - \ + && apt-get install -y nodejs \ + && npm install -g npm + +# Create sail user +RUN groupadd --force -g $WWWGROUP sail \ + && useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail + +CMD ["bash"] diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..81bfb13 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,27 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile.dev + image: chat-app-dev + extra_hosts: + - 'host.docker.internal:host-gateway' + ports: + - '${APP_PORT:-8000}:8000' + - '${VITE_PORT:-5173}:${VITE_PORT:-5173}' + environment: + APP_ENV: testing + WWWUSER: '${WWWUSER:-1000}' + WWWGROUP: '${WWWGROUP:-1000}' + volumes: + - '.:/var/www/html' + - '/var/www/html/vendor' + - '/var/www/html/node_modules' + networks: + - sail + tty: true + stdin_open: true + +networks: + sail: + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..0e4be13 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,37 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + image: chat-app + extra_hosts: + - 'host.docker.internal:host-gateway' + ports: + - '${APP_PORT:-80}:80' + - '${VITE_PORT:-5173}:${VITE_PORT:-5173}' + environment: + WWWUSER: '${WWWUSER:-1000}' + WWWGROUP: '${WWWGROUP:-1000}' + LARAVEL_SAIL: 1 + XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}' + XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}' + IGNITION_LOCAL_SITES_PATH: '${PWD}' + volumes: + - '.:/var/www/html' + - 'sail-playwright:/var/www/html/.playwright' + networks: + - sail + depends_on: [] + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/up"] + interval: 10s + timeout: 5s + retries: 3 + +networks: + sail: + driver: bridge + +volumes: + sail-playwright: + driver: local diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..5e4cc9e --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -e + +# Setup environment if not exists +if [ ! -f ".env" ]; then + cp .env.example .env + php artisan key:generate +fi + +# Create SQLite database if not exists +if [ ! -f "database/database.sqlite" ]; then + touch database/database.sqlite +fi + +# Run migrations +php artisan migrate --force + +# Clear caches +php artisan config:clear +php artisan cache:clear + +# Start PHP built-in server +exec php artisan serve --host=0.0.0.0 --port=80 diff --git a/scripts/docker-test.sh b/scripts/docker-test.sh new file mode 100755 index 0000000..fad8a99 --- /dev/null +++ b/scripts/docker-test.sh @@ -0,0 +1,29 @@ +#!/bin/bash +# Run browser tests in Docker +# Usage: ./scripts/docker-test.sh [filter] + +set -e + +FILTER=${1:-"Browser"} + +echo "๐Ÿณ Building development container..." +docker compose -f docker-compose.dev.yml build + +echo "๐Ÿ“ฆ Installing dependencies..." +docker compose -f docker-compose.dev.yml run --rm app bash -c " + composer install --no-interaction + npm ci + npx playwright install chromium --with-deps + npm run build +" + +echo "๐Ÿงช Running browser tests..." +docker compose -f docker-compose.dev.yml run --rm app bash -c " + cp .env.example .env 2>/dev/null || true + php artisan key:generate --force + touch database/database.sqlite + php artisan migrate --force + php artisan test --filter=$FILTER +" + +echo "โœ… Done!" From 23af9f7c0146597032ce9aa41c98a0fbea6f5d29 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 13 Dec 2025 05:54:21 +0000 Subject: [PATCH 2/3] feat: full Docker stack with Postgres, Redis, Horizon, pgvector Infrastructure as code for complete Laravel development: Services: - app: PHP 8.4 Laravel application - vite: Node 22 dev server for HMR - horizon: Queue worker for background jobs - scheduler: Laravel task scheduler - postgres: PostgreSQL 16 with pgvector for embeddings - redis: Redis 7 for cache/sessions/queues - mailpit: Email testing UI - playwright: Browser testing (profile: testing) Scripts: - scripts/docker-up.sh: Start the stack - scripts/docker-test.sh: Run unit/feature tests - scripts/docker-browser-test.sh: Run Pest 4 browser tests - scripts/docker-fresh.sh: Fresh install with migrate:fresh Removed simpler single-container setup in favor of full stack. --- .dockerignore | 28 +++-- Dockerfile | 88 ---------------- Dockerfile.dev | 41 -------- docker-compose.dev.yml | 27 ----- docker-compose.yml | 183 ++++++++++++++++++++++++++++++--- docker-entrypoint.sh | 23 ----- docker/app/Dockerfile | 52 ++++++++++ docker/app/php.ini | 17 +++ docker/node/Dockerfile | 19 ++++ docker/playwright/Dockerfile | 28 +++++ docker/postgres/init.sql | 6 ++ scripts/docker-browser-test.sh | 14 +++ scripts/docker-fresh.sh | 20 ++++ scripts/docker-test.sh | 33 ++---- scripts/docker-up.sh | 23 +++++ 15 files changed, 372 insertions(+), 230 deletions(-) delete mode 100644 Dockerfile delete mode 100644 Dockerfile.dev delete mode 100644 docker-compose.dev.yml delete mode 100644 docker-entrypoint.sh create mode 100644 docker/app/Dockerfile create mode 100644 docker/app/php.ini create mode 100644 docker/node/Dockerfile create mode 100644 docker/playwright/Dockerfile create mode 100644 docker/postgres/init.sql create mode 100755 scripts/docker-browser-test.sh create mode 100755 scripts/docker-fresh.sh create mode 100755 scripts/docker-up.sh diff --git a/.dockerignore b/.dockerignore index c85a817..4ee7c7f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,14 +1,20 @@ -/.git -/node_modules -/vendor -/.env -/.idea -/.vscode -/storage/logs/* -/storage/framework/cache/* -/storage/framework/sessions/* -/storage/framework/views/* -/bootstrap/cache/* +.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/Dockerfile b/Dockerfile deleted file mode 100644 index 70b39db..0000000 --- a/Dockerfile +++ /dev/null @@ -1,88 +0,0 @@ -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 \ - sqlite3 \ - libcap2-bin \ - libpng-dev \ - libonig-dev \ - libxml2-dev \ - libzip-dev \ - libsqlite3-dev \ - libnss3 \ - libatk1.0-0 \ - libatk-bridge2.0-0 \ - libcups2 \ - libdrm2 \ - libxkbcommon0 \ - libxcomposite1 \ - libxdamage1 \ - libxfixes3 \ - libxrandr2 \ - libgbm1 \ - libasound2 \ - libpango-1.0-0 \ - libcairo2 \ - libatspi2.0-0 \ - libgtk-3-0 \ - && apt-get clean \ - && rm -rf /var/lib/apt/lists/* - -# Install PHP extensions -RUN docker-php-ext-install pdo pdo_sqlite mbstring exif pcntl bcmath gd zip - -# Install Composer -COPY --from=composer:latest /usr/bin/composer /usr/bin/composer - -# Install Node.js -RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - \ - && apt-get install -y nodejs \ - && npm install -g npm - -# 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 -RUN composer install --no-interaction --prefer-dist --optimize-autoloader -RUN npm ci - -# Install Playwright browsers -RUN npx playwright install chromium --with-deps - -# Build frontend -RUN npm run build - -# Setup permissions -RUN chown -R sail:sail /var/www/html \ - && chmod -R 755 /var/www/html/storage - -COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh -RUN chmod +x /usr/local/bin/docker-entrypoint.sh - -EXPOSE 80 5173 - -ENTRYPOINT ["docker-entrypoint.sh"] diff --git a/Dockerfile.dev b/Dockerfile.dev deleted file mode 100644 index 18f4f5a..0000000 --- a/Dockerfile.dev +++ /dev/null @@ -1,41 +0,0 @@ -FROM php:8.4-cli - -LABEL maintainer="Chat App Dev" - -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 (including Playwright requirements) -RUN apt-get update && apt-get install -y \ - gnupg gosu curl ca-certificates zip unzip git sqlite3 \ - libpng-dev libonig-dev libxml2-dev libzip-dev libsqlite3-dev \ - # Playwright/Chromium dependencies - libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 \ - libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 \ - libgbm1 libasound2 libpango-1.0-0 libcairo2 libatspi2.0-0 \ - libgtk-3-0 fonts-liberation libappindicator3-1 xdg-utils \ - && apt-get clean && rm -rf /var/lib/apt/lists/* - -# Install PHP extensions -RUN docker-php-ext-install pdo pdo_sqlite mbstring exif pcntl bcmath gd zip - -# Install Composer -COPY --from=composer:latest /usr/bin/composer /usr/bin/composer - -# Install Node.js -RUN curl -fsSL https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - \ - && apt-get install -y nodejs \ - && npm install -g npm - -# Create sail user -RUN groupadd --force -g $WWWGROUP sail \ - && useradd -ms /bin/bash --no-user-group -g $WWWGROUP -u 1337 sail - -CMD ["bash"] diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml deleted file mode 100644 index 81bfb13..0000000 --- a/docker-compose.dev.yml +++ /dev/null @@ -1,27 +0,0 @@ -services: - app: - build: - context: . - dockerfile: Dockerfile.dev - image: chat-app-dev - extra_hosts: - - 'host.docker.internal:host-gateway' - ports: - - '${APP_PORT:-8000}:8000' - - '${VITE_PORT:-5173}:${VITE_PORT:-5173}' - environment: - APP_ENV: testing - WWWUSER: '${WWWUSER:-1000}' - WWWGROUP: '${WWWGROUP:-1000}' - volumes: - - '.:/var/www/html' - - '/var/www/html/vendor' - - '/var/www/html/node_modules' - networks: - - sail - tty: true - stdin_open: true - -networks: - sail: - driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml index 0e4be13..5ff33ff 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,37 +1,188 @@ services: + # Laravel Application app: build: context: . - dockerfile: Dockerfile - image: chat-app + dockerfile: docker/app/Dockerfile + container_name: chat-app + restart: unless-stopped extra_hosts: - 'host.docker.internal:host-gateway' ports: - - '${APP_PORT:-80}:80' - - '${VITE_PORT:-5173}:${VITE_PORT:-5173}' + - '${APP_PORT:-8000}:8000' environment: - WWWUSER: '${WWWUSER:-1000}' - WWWGROUP: '${WWWGROUP:-1000}' - LARAVEL_SAIL: 1 - XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}' - XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}' - IGNITION_LOCAL_SITES_PATH: '${PWD}' + 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' - - 'sail-playwright:/var/www/html/.playwright' + - './docker/app/php.ini:/usr/local/etc/php/conf.d/custom.ini:ro' networks: - - sail - depends_on: [] + - chat + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy healthcheck: - test: ["CMD", "curl", "-f", "http://localhost/up"] + 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: - sail: + chat: driver: bridge volumes: - sail-playwright: + chat-postgres: + driver: local + chat-redis: driver: local diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh deleted file mode 100644 index 5e4cc9e..0000000 --- a/docker-entrypoint.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/bin/bash -set -e - -# Setup environment if not exists -if [ ! -f ".env" ]; then - cp .env.example .env - php artisan key:generate -fi - -# Create SQLite database if not exists -if [ ! -f "database/database.sqlite" ]; then - touch database/database.sqlite -fi - -# Run migrations -php artisan migrate --force - -# Clear caches -php artisan config:clear -php artisan cache:clear - -# Start PHP built-in server -exec php artisan serve --host=0.0.0.0 --port=80 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 index fad8a99..d661ba9 100755 --- a/scripts/docker-test.sh +++ b/scripts/docker-test.sh @@ -1,29 +1,14 @@ #!/bin/bash -# Run browser tests in Docker -# Usage: ./scripts/docker-test.sh [filter] - +# Run tests in Docker (including browser tests) set -e -FILTER=${1:-"Browser"} - -echo "๐Ÿณ Building development container..." -docker compose -f docker-compose.dev.yml build - -echo "๐Ÿ“ฆ Installing dependencies..." -docker compose -f docker-compose.dev.yml run --rm app bash -c " - composer install --no-interaction - npm ci - npx playwright install chromium --with-deps - npm run build -" +FILTER=${1:-""} -echo "๐Ÿงช Running browser tests..." -docker compose -f docker-compose.dev.yml run --rm app bash -c " - cp .env.example .env 2>/dev/null || true - php artisan key:generate --force - touch database/database.sqlite - php artisan migrate --force - php artisan test --filter=$FILTER -" +echo "๐Ÿงช Running tests in Docker..." -echo "โœ… Done!" +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" From c4e4cd95f72eb608ce892d28cd2d2575dda60bbf Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 13 Dec 2025 06:30:11 +0000 Subject: [PATCH 3/3] feat: add UI Review Agent browser test suite Comprehensive Pest 4 browser test that acts as automated UI reviewer: - Public pages review (welcome, login, register) - Dashboard review (empty and with data) - Chat pages review (index, show, with messages) - Settings pages review (profile, password, appearance, 2FA, providers) - Navigation flow verification - Responsive breakpoints (320px to 1920px) - Dark mode testing on all pages - Mobile viewport testing - Basic accessibility checks - Error state review (404) - Performance baseline check Screenshots saved to storage/app/screenshots/review/ for visual inspection. Run with: php artisan test tests/Browser/UiReviewAgentTest.php --- tests/Browser/UiReviewAgentTest.php | 512 ++++++++++++++++++++++++++++ 1 file changed, 512 insertions(+) create mode 100644 tests/Browser/UiReviewAgentTest.php 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); + }); +});