diff --git a/.github/workflows/frontend.yml b/.github/workflows/frontend.yml index d0eb6e9..75cc076 100644 --- a/.github/workflows/frontend.yml +++ b/.github/workflows/frontend.yml @@ -65,6 +65,7 @@ jobs: name: E2E Tests runs-on: ubuntu-latest needs: lint-and-test + timeout-minutes: 20 services: postgres: @@ -100,9 +101,33 @@ jobs: working-directory: ./frontend run: npm ci + - name: Verify PostgreSQL is ready + run: | + echo "🐘 Checking PostgreSQL connection..." + for i in {1..30}; do + if pg_isready -h localhost -p 5432 -U postgres > /dev/null 2>&1; then + echo "✅ PostgreSQL is ready!" + break + fi + if [ $i -eq 30 ]; then + echo "❌ PostgreSQL failed to become ready" + exit 1 + fi + echo "Waiting for PostgreSQL... ($i/30)" + sleep 1 + done + + echo "📊 PostgreSQL status:" + psql -h localhost -U postgres -d stackshare_test -c "SELECT version();" || echo "Failed to query PostgreSQL" + env: + PGPASSWORD: postgres + - name: Install Playwright Browsers working-directory: ./frontend - run: npx playwright install --with-deps + run: | + echo "🎭 Installing Playwright browsers..." + npx playwright install --with-deps + echo "✅ Playwright browsers installed" - name: Restore backend dependencies working-directory: ./backend @@ -112,28 +137,188 @@ jobs: working-directory: ./backend run: dotnet build --no-restore - - name: Start backend services + - name: Start backend services with detailed logging working-directory: ./backend run: | - dotnet run --project src/StackShare.API & - sleep 30 # Wait for backend to start + echo "🚀 Starting backend API..." + echo "Environment: ASPNETCORE_ENVIRONMENT=Testing" + echo "Database: stackshare_test on port 5432" + + # Start API in background with logging + nohup dotnet run --project src/StackShare.API > ../api.log 2>&1 & + API_PID=$! + echo "📊 API started with PID: $API_PID" + + # Wait and check for API startup + echo "⏳ Waiting for API to start (up to 60 seconds)..." + for i in {1..60}; do + echo "Attempt $i/60: Checking API health..." + + if curl -f -s http://localhost:5095/api/health > /dev/null 2>&1; then + echo "✅ API is healthy and ready!" + echo "📋 API logs (last 20 lines):" + tail -20 ../api.log || echo "No logs available yet" + break + fi + + if [ $i -eq 60 ]; then + echo "❌ API failed to start within 60 seconds" + echo "📋 Full API logs:" + cat ../api.log || echo "No logs found" + echo "🔍 Checking if process is still running:" + ps aux | grep dotnet | grep -v grep || echo "No dotnet processes found" + echo "🔍 Checking port 5095:" + netstat -tlnp | grep 5095 || echo "Port 5095 not in use" + exit 1 + fi + + sleep 1 + done + + echo "🎯 Final API health check:" + curl -v http://localhost:5095/api/health || true env: ASPNETCORE_ENVIRONMENT: Testing ConnectionStrings__DefaultConnection: "Host=localhost;Port=5432;Database=stackshare_test;Username=postgres;Password=postgres" - - name: Build frontend + - name: Build frontend with logging working-directory: ./frontend - run: npm run build + run: | + echo "🏗️ Building frontend..." + npm run build + echo "✅ Frontend build completed" + echo "📂 Build output:" + ls -la dist/ || echo "No dist folder found" - - name: Start frontend + - name: Start frontend with detailed monitoring working-directory: ./frontend run: | - npm run preview & - sleep 10 # Wait for frontend to start + echo "🌐 Starting frontend preview server..." + + # Start preview in background with logging + nohup npm run preview > ../frontend.log 2>&1 & + FRONTEND_PID=$! + echo "📊 Frontend started with PID: $FRONTEND_PID" + + # Wait and check for frontend startup + echo "⏳ Waiting for frontend to start (up to 30 seconds)..." + for i in {1..30}; do + echo "Attempt $i/30: Checking frontend availability..." + + if curl -f -s http://localhost:4173 > /dev/null 2>&1; then + echo "✅ Frontend is ready!" + echo "📋 Frontend logs (last 10 lines):" + tail -10 ../frontend.log || echo "No logs available yet" + break + fi + + if [ $i -eq 30 ]; then + echo "❌ Frontend failed to start within 30 seconds" + echo "📋 Full frontend logs:" + cat ../frontend.log || echo "No logs found" + echo "🔍 Checking if process is still running:" + ps aux | grep "npm run preview" | grep -v grep || echo "No preview process found" + echo "🔍 Checking port 4173:" + netstat -tlnp | grep 4173 || echo "Port 4173 not in use" + exit 1 + fi + + sleep 1 + done + + echo "🎯 Final frontend check:" + curl -I http://localhost:4173 || true + + - name: Pre-test system diagnostics + run: | + echo "🔍 System diagnostics before running tests:" + echo "Memory usage:" + free -h + echo "Disk usage:" + df -h + echo "Running processes:" + ps aux | head -20 + echo "Network connections:" + netstat -tlnp | grep -E "(4173|5095|5432)" + echo "Environment variables:" + env | grep -E "(CI|NODE_|ASPNETCORE_)" | sort - - name: Run Playwright tests + - name: Run Playwright tests with detailed monitoring working-directory: ./frontend - run: npm run test:e2e + run: | + echo "🎭 Starting Playwright E2E tests..." + echo "Configuration check:" + echo "- CI environment: $CI" + echo "- Base URL will be: http://localhost:4173" + echo "- Workers: 2 (CI mode)" + echo "- Browsers: Chromium only (CI mode)" + + # Verify services are still running + echo "🔍 Pre-test service verification:" + if curl -f -s http://localhost:4173 > /dev/null; then + echo "✅ Frontend is responsive" + else + echo "❌ Frontend is not responsive" + curl -I http://localhost:4173 || true + fi + + if curl -f -s http://localhost:5095/api/health > /dev/null; then + echo "✅ Backend API is responsive" + else + echo "❌ Backend API is not responsive" + curl -I http://localhost:5095/api/health || true + fi + + # Run tests with timeout and progress reporting + echo "🚀 Starting test execution..." + timeout 600 npm run test:e2e || { + EXIT_CODE=$? + echo "❌ Tests failed or timed out (exit code: $EXIT_CODE)" + + echo "📋 Recent frontend logs:" + tail -50 ../frontend.log || echo "No frontend logs" + + echo "📋 Recent API logs:" + tail -50 ../api.log || echo "No API logs" + + echo "🔍 Current system state:" + ps aux | grep -E "(dotnet|npm|node)" | grep -v grep || echo "No relevant processes" + netstat -tlnp | grep -E "(4173|5095)" || echo "Services not listening" + + exit $EXIT_CODE + } + + echo "✅ All E2E tests completed successfully!" + + - name: Collect logs and diagnostics on failure + if: failure() + run: | + echo "📋 Collecting diagnostic information..." + + echo "=== FRONTEND LOGS ===" + cat frontend.log 2>/dev/null || echo "No frontend logs found" + + echo "=== API LOGS ===" + cat api.log 2>/dev/null || echo "No API logs found" + + echo "=== SYSTEM STATE ===" + echo "Processes:" + ps aux | grep -E "(dotnet|npm|node|playwright)" | grep -v grep || echo "No relevant processes" + + echo "Network:" + netstat -tlnp | grep -E "(4173|5095|5432)" || echo "No services listening" + + echo "Memory:" + free -h + + echo "Disk:" + df -h + + echo "=== PLAYWRIGHT TEST RESULTS ===" + ls -la frontend/test-results/ 2>/dev/null || echo "No test results found" + + # Try to get more info about what failed + find frontend/test-results/ -name "*.txt" -exec echo "--- {} ---" \; -exec cat {} \; 2>/dev/null || true - name: Upload Playwright report uses: actions/upload-artifact@v4 @@ -142,6 +327,17 @@ jobs: name: playwright-report path: frontend/playwright-report/ retention-days: 30 + + - name: Upload test results and logs + uses: actions/upload-artifact@v4 + if: always() + with: + name: e2e-logs-and-results + path: | + frontend/test-results/ + api.log + frontend.log + retention-days: 7 build-and-push: name: Build and Push Docker Image diff --git a/.gitignore b/.gitignore index ce89292..fa48f61 100644 --- a/.gitignore +++ b/.gitignore @@ -301,6 +301,19 @@ FakesAssemblies/ .ntvs_analysis.dat node_modules/ +# Frontend build and test artifacts +frontend/dist/ +frontend/build/ +frontend/.next/ +frontend/out/ +frontend/coverage/ +frontend/playwright-report/ +frontend/test-results/ +frontend/.env.local +frontend/.env.development.local +frontend/.env.test.local +frontend/.env.production.local + # Visual Studio 6 build log *.plg diff --git a/frontend/playwright-report/index.html b/frontend/playwright-report/index.html deleted file mode 100644 index d7a4517..0000000 --- a/frontend/playwright-report/index.html +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - Playwright Test Report - - - - -
- - - \ No newline at end of file diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index cca269f..5c0917c 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -11,20 +11,27 @@ export default defineConfig({ forbidOnly: !!process.env.CI, /* Retry on CI only */ retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, + /* Use multiple workers on CI for faster execution */ + workers: process.env.CI ? 2 : undefined, + /* Global test timeout */ + timeout: process.env.CI ? 60 * 1000 : 30 * 1000, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: 'html', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: 'http://localhost:5173', + baseURL: process.env.CI ? 'http://localhost:4173' : 'http://localhost:5173', /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: 'on-first-retry', }, /* Configure projects for major browsers */ - projects: [ + projects: process.env.CI ? [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ] : [ { name: 'chromium', use: { ...devices['Desktop Chrome'] }, @@ -62,9 +69,9 @@ export default defineConfig({ ], /* Run your local dev server before starting the tests */ - webServer: { + webServer: process.env.CI ? undefined : { command: 'npm run dev', url: 'http://localhost:5173', - reuseExistingServer: !process.env.CI, + reuseExistingServer: true, }, }); \ No newline at end of file diff --git a/frontend/src/components/ui/form-hooks.ts b/frontend/src/components/ui/form-hooks.ts new file mode 100644 index 0000000..c4858c4 --- /dev/null +++ b/frontend/src/components/ui/form-hooks.ts @@ -0,0 +1,51 @@ +"use client" + +import * as React from "react" +import { useFormContext } from "react-hook-form" + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +type FormItemContextValue = { + id: string +} + +import type { + FieldPath, + FieldValues, +} from "react-hook-form" + +export const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +export const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +export const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} \ No newline at end of file diff --git a/frontend/src/components/ui/form.tsx b/frontend/src/components/ui/form.tsx index 3bd8bc8..956a280 100644 --- a/frontend/src/components/ui/form.tsx +++ b/frontend/src/components/ui/form.tsx @@ -6,7 +6,6 @@ import { Slot } from "@radix-ui/react-slot" import { Controller, FormProvider, - useFormContext, } from "react-hook-form" import type { ControllerProps, @@ -16,20 +15,10 @@ import type { import { cn } from "@/lib/utils" import { Label } from "@/components/ui/label" +import { useFormField, FormFieldContext, FormItemContext } from "./form-hooks" const Form = FormProvider -type FormFieldContextValue< - TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath -> = { - name: TName -} - -const FormFieldContext = React.createContext( - {} as FormFieldContextValue -) - const FormField = < TFieldValues extends FieldValues = FieldValues, TName extends FieldPath = FieldPath @@ -43,37 +32,6 @@ const FormField = < ) } -const useFormField = () => { - const fieldContext = React.useContext(FormFieldContext) - const itemContext = React.useContext(FormItemContext) - const { getFieldState, formState } = useFormContext() - - const fieldState = getFieldState(fieldContext.name, formState) - - if (!fieldContext) { - throw new Error("useFormField should be used within ") - } - - const { id } = itemContext - - return { - id, - name: fieldContext.name, - formItemId: `${id}-form-item`, - formDescriptionId: `${id}-form-item-description`, - formMessageId: `${id}-form-item-message`, - ...fieldState, - } -} - -type FormItemContextValue = { - id: string -} - -const FormItemContext = React.createContext( - {} as FormItemContextValue -) - const FormItem = React.forwardRef< HTMLDivElement, React.HTMLAttributes @@ -169,7 +127,6 @@ const FormMessage = React.forwardRef< FormMessage.displayName = "FormMessage" export { - useFormField, Form, FormItem, FormLabel, diff --git a/frontend/src/pages/create-stack.tsx b/frontend/src/pages/create-stack.tsx index 4af2e42..71bcc07 100644 --- a/frontend/src/pages/create-stack.tsx +++ b/frontend/src/pages/create-stack.tsx @@ -3,20 +3,28 @@ import { toast } from 'sonner' import { StackForm } from '@/components/stack-form' import { useCreateStack } from '@/hooks/use-stacks' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' -import type { CreateStackRequest } from '@/types/stack' +import type { CreateStackRequest, Technology, StackType } from '@/types/stack' + +interface StackFormData { + name: string + description: string + type: StackType + isPublic: boolean + technologies: Technology[] +} export default function CreateStackPage() { const navigate = useNavigate() const createStackMutation = useCreateStack() - const handleSubmit = async (formData: any) => { + const handleSubmit = async (formData: StackFormData) => { try { const stackData: CreateStackRequest = { name: formData.name, description: formData.description, type: formData.type, isPublic: formData.isPublic, - technologyIds: formData.technologies.map((tech: any) => tech.id) + technologyIds: formData.technologies.map((tech: Technology) => tech.id) } await createStackMutation.mutateAsync(stackData) diff --git a/frontend/src/pages/edit-stack.tsx b/frontend/src/pages/edit-stack.tsx index 3da5b39..9a7ef2c 100644 --- a/frontend/src/pages/edit-stack.tsx +++ b/frontend/src/pages/edit-stack.tsx @@ -6,7 +6,15 @@ import { StackForm } from '@/components/stack-form' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { useAuth } from '@/features/auth/use-auth' -import type { UpdateStackRequest } from '@/types/stack' +import type { UpdateStackRequest, Technology, StackType } from '@/types/stack' + +interface StackFormData { + name: string + description: string + type: StackType + isPublic: boolean + technologies: Technology[] +} export default function EditStackPage() { const { id } = useParams<{ id: string }>() @@ -16,7 +24,7 @@ export default function EditStackPage() { const { data: stack, isLoading, isError } = useStack(id!) const updateStackMutation = useUpdateStack() - const handleSubmit = async (formData: any) => { + const handleSubmit = async (formData: StackFormData) => { if (!stack) return try { @@ -25,7 +33,7 @@ export default function EditStackPage() { description: formData.description, type: formData.type, isPublic: formData.isPublic, - technologyIds: formData.technologies.map((tech: any) => tech.id) + technologyIds: formData.technologies.map((tech: Technology) => tech.id) } await updateStackMutation.mutateAsync({ id: stack.id, data: stackData }) diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts index 5d6d1d2..0505dc8 100644 --- a/frontend/vitest.config.ts +++ b/frontend/vitest.config.ts @@ -15,5 +15,13 @@ export default defineConfig({ globals: true, environment: 'jsdom', setupFiles: './src/test/setup.ts', + exclude: [ + '**/node_modules/**', + '**/dist/**', + '**/cypress/**', + '**/.{idea,git,cache,output,temp}/**', + '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*', + '**/e2e/**' + ], }, }) \ No newline at end of file