# Know Foolery - Development Guidelines & Coding Standards ## Overview This document establishes coding standards, development practices, and guidelines for the Know Foolery project to ensure consistency, maintainability, and quality across the codebase. ## Code Organization ### Project Structure Standards ``` knowfoolery/ ├── backend/ │ ├── services/ # Microservices │ │ ├── {service-name}/ │ │ │ ├── cmd/ │ │ │ │ └── main.go # Service entry point │ │ │ ├── internal/ │ │ │ │ ├── handlers/ # Fiber HTTP handlers │ │ │ │ ├── services/ # Business logic │ │ │ │ ├── middleware/ # Service-specific middleware │ │ │ │ └── models/ # Ent models and schemas │ │ │ ├── api/ # API definitions (OpenAPI/gRPC) │ │ │ ├── config/ # Configuration management │ │ │ ├── tests/ # Service-specific tests │ │ │ ├── Dockerfile # Container definition │ │ │ └── go.mod # Go module │ │ └── shared/ # Shared packages │ │ ├── auth/ # JWT middleware & Zitadel integration │ │ ├── database/ # Ent database client │ │ ├── observability/ # Metrics and tracing │ │ ├── security/ # Security utilities │ │ └── utils/ # Common utilities ├── frontend/ │ ├── packages/ │ │ ├── ui-components/ # Shared Gluestack UI components │ │ ├── shared-logic/ # Business logic │ │ ├── shared-types/ # TypeScript types │ │ └── shared-utils/ # Utility functions │ ├── apps/ │ │ ├── web/ # React web app │ │ ├── mobile/ # React Native app │ │ └── desktop/ # Wails desktop app └── infrastructure/ # DevOps and deployment ``` ### Naming Conventions #### Go Code ```go // Files: snake_case user_service.go jwt_middleware.go database_client.go // Packages: lowercase, single word when possible package auth package database package observability // Exported types: PascalCase type GameService struct {} type AuthClaims struct {} type DatabaseConfig struct {} // Unexported types: camelCase type userRepository struct {} type tokenValidator struct {} // Functions: PascalCase (exported), camelCase (unexported) func NewGameService() *GameService {} func (s *GameService) StartGame() error {} func validateToken() error {} // Constants: PascalCase or SCREAMING_SNAKE_CASE for groups const DefaultTimeout = 30 * time.Second const ( StatusActive = "active" StatusInactive = "inactive" ) // Interfaces: noun or adjective + "er" suffix type TokenValidator interface {} type UserRepository interface {} type ScoreCalculator interface {} ``` #### TypeScript Code ```typescript // Files: kebab-case game-card.tsx user-service.ts auth-context.tsx // Components: PascalCase export const GameCard: React.FC = () => {} export const LeaderboardList: React.FC = () => {} // Hooks: camelCase with "use" prefix export const useGameSession = () => {} export const useAuthentication = () => {} // Functions: camelCase export const calculateScore = () => {} export const validateAnswer = () => {} // Types/Interfaces: PascalCase interface GameSessionProps {} type AuthenticationState = 'loading' | 'authenticated' | 'unauthenticated' // Constants: SCREAMING_SNAKE_CASE export const MAX_ATTEMPTS = 3 export const SESSION_TIMEOUT = 30 * 60 * 1000 // 30 minutes ``` ## Go Development Standards ### Fiber Framework Patterns #### Service Structure ```go // Service initialization with Fiber package main import ( "log" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/middleware/cors" "github.com/gofiber/fiber/v3/middleware/logger" "github.com/gofiber/fiber/v3/middleware/recover" ) func main() { // Create Fiber app with configuration app := fiber.New(fiber.Config{ AppName: "Know Foolery Game Service", ServerHeader: "Fiber", ErrorHandler: customErrorHandler, ReadTimeout: time.Second * 10, WriteTimeout: time.Second * 10, IdleTimeout: time.Second * 120, }) // Global middleware app.Use(logger.New()) app.Use(recover.New()) app.Use(cors.New(cors.Config{ AllowOrigins: "http://localhost:3000, https://app.knowfoolery.com", AllowMethods: "GET,POST,PUT,DELETE,OPTIONS", AllowHeaders: "Origin, Content-Type, Authorization", })) // Health check endpoint app.Get("/health", healthCheck) // API routes with versioning api := app.Group("/api/v1") setupGameRoutes(api) // Start server log.Fatal(app.Listen(":8080")) } func customErrorHandler(c *fiber.Ctx, err error) error { code := fiber.StatusInternalServerError message := "Internal Server Error" if e, ok := err.(*fiber.Error); ok { code = e.Code message = e.Message } return c.Status(code).JSON(fiber.Map{ "error": true, "message": message, "code": code, }) } ``` #### Handler Patterns ```go // Handler structure and patterns package handlers import ( "github.com/gofiber/fiber/v3" "knowfoolery/internal/services" ) type GameHandler struct { gameService *services.GameService validator *validator.Validate } func NewGameHandler(gameService *services.GameService) *GameHandler { return &GameHandler{ gameService: gameService, validator: validator.New(), } } // HTTP handler with proper error handling func (h *GameHandler) StartGame(c *fiber.Ctx) error { var req StartGameRequest // Parse and validate request if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": true, "message": "Invalid request body", }) } if err := h.validator.Struct(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": true, "message": "Validation failed", "details": err.Error(), }) } // Extract user context from JWT middleware userID := c.Locals("user_id").(string) userRole := c.Locals("user_role").(string) // Business logic gameSession, err := h.gameService.StartGame(c.Context(), userID, req.PlayerName) if err != nil { return handleServiceError(c, err) } return c.Status(fiber.StatusCreated).JSON(fiber.Map{ "success": true, "data": gameSession, }) } // Centralized error handling func handleServiceError(c *fiber.Ctx, err error) error { switch { case errors.Is(err, services.ErrInvalidInput): return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": true, "message": "Invalid input", "details": err.Error(), }) case errors.Is(err, services.ErrNotFound): return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ "error": true, "message": "Resource not found", }) case errors.Is(err, services.ErrUnauthorized): return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": true, "message": "Unauthorized access", }) default: return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": true, "message": "Internal server error", }) } } // Request/Response structures with validation type StartGameRequest struct { PlayerName string `json:"player_name" validate:"required,min=2,max=50,alphanum_space"` } type StartGameResponse struct { SessionID string `json:"session_id"` PlayerName string `json:"player_name"` StartTime time.Time `json:"start_time"` TimeRemaining int `json:"time_remaining_seconds"` } ``` #### Middleware Patterns ```go // Custom middleware for authentication package middleware import ( "github.com/gofiber/fiber/v3" "knowfoolery/shared/auth" ) func JWTMiddleware(zitadelRepo auth.ZitadelRepository) fiber.Handler { return func(c *fiber.Ctx) error { // Extract token from Authorization header authHeader := c.Get("Authorization") if authHeader == "" { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": true, "message": "Authorization header required", }) } token := strings.TrimPrefix(authHeader, "Bearer ") if token == authHeader { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": true, "message": "Invalid authorization header format", }) } // Validate token claims, err := zitadelRepo.ValidateToken(c.Context(), token) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": true, "message": "Invalid token", }) } // Set user context c.Locals("user_id", claims.Subject) c.Locals("user_email", claims.Email) c.Locals("user_name", claims.Name) c.Locals("user_roles", claims.Roles) return c.Next() } } // Rate limiting middleware func RateLimitMiddleware(redisClient *redis.Client) fiber.Handler { return func(c *fiber.Ctx) error { userID := c.Locals("user_id") clientIP := c.IP() // Per-user rate limiting if userID != nil { userKey := fmt.Sprintf("rate_limit:user:%s", userID) if !allowRequest(redisClient, userKey, 60, time.Minute) { return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{ "error": true, "message": "Rate limit exceeded for user", }) } } // Per-IP rate limiting ipKey := fmt.Sprintf("rate_limit:ip:%s", clientIP) if !allowRequest(redisClient, ipKey, 100, time.Minute) { return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{ "error": true, "message": "Rate limit exceeded for IP", }) } return c.Next() } } ``` ### Database Patterns with Ent #### Schema Definition Standards ```go // Ent schema with proper field validation and indexes package schema import ( "entgo.io/ent" "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" "entgo.io/ent/schema/index" "time" ) type Question struct { ent.Schema } func (Question) Fields() []ent.Field { return []ent.Field{ field.String("theme"). NotEmpty(). MaxLen(100). Comment("Question category/theme"), field.Text("text"). NotEmpty(). MaxLen(1000). Comment("The actual question text"), field.String("answer"). NotEmpty(). MaxLen(500). Comment("Correct answer"), field.Text("hint"). Optional(). MaxLen(500). Comment("Optional hint text"), field.Enum("difficulty"). Values("easy", "medium", "hard"). Default("medium"). Comment("Question difficulty level"), field.Bool("is_active"). Default(true). Comment("Whether question is active"), field.Time("created_at"). Default(time.Now). Immutable(). Comment("Creation timestamp"), field.Time("updated_at"). Default(time.Now). UpdateDefault(time.Now). Comment("Last update timestamp"), } } func (Question) Indexes() []ent.Index { return []ent.Index{ index.Fields("theme", "is_active"), index.Fields("difficulty", "is_active"), index.Fields("created_at"), } } func (Question) Edges() []ent.Edge { return []ent.Edge{ edge.To("attempts", QuestionAttempt.Type), } } ``` #### Service Layer Patterns ```go // Service layer with proper error handling package services import ( "context" "errors" "knowfoolery/internal/models" "knowfoolery/ent" ) var ( ErrInvalidInput = errors.New("invalid input") ErrNotFound = errors.New("resource not found") ErrUnauthorized = errors.New("unauthorized access") ErrGameInProgress = errors.New("game already in progress") ErrSessionExpired = errors.New("session expired") ) type GameService struct { client *ent.Client questionSvc QuestionService sessionSvc SessionService validator *validator.Validate } func NewGameService( client *ent.Client, questionSvc QuestionService, sessionSvc SessionService, ) *GameService { return &GameService{ client: client, questionSvc: questionSvc, sessionSvc: sessionSvc, validator: validator.New(), } } func (s *GameService) StartGame(ctx context.Context, userID, playerName string) (*models.GameSession, error) { // Validate input if err := s.validatePlayerName(playerName); err != nil { return nil, fmt.Errorf("%w: %v", ErrInvalidInput, err) } // Check for existing active session activeSession, err := s.client.GameSession. Query(). Where(gamesession.And( gamesession.PlayerNameEQ(playerName), gamesession.StatusEQ(gamesession.StatusActive), )). Only(ctx) if err != nil && !ent.IsNotFound(err) { return nil, fmt.Errorf("failed to check existing session: %w", err) } if activeSession != nil { return nil, ErrGameInProgress } // Start database transaction tx, err := s.client.Tx(ctx) if err != nil { return nil, fmt.Errorf("failed to start transaction: %w", err) } defer tx.Rollback() // Create new game session session, err := tx.GameSession. Create(). SetPlayerName(playerName). SetTotalScore(0). SetQuestionsAsked(0). SetQuestionsCorrect(0). SetHintsUsed(0). SetStartTime(time.Now()). SetStatus(gamesession.StatusActive). Save(ctx) if err != nil { return nil, fmt.Errorf("failed to create session: %w", err) } // Get first question question, err := s.questionSvc.GetRandomQuestion(ctx) if err != nil { return nil, fmt.Errorf("failed to get question: %w", err) } // Update session with first question session, err = session.Update(). SetCurrentQuestionID(question.ID). SetCurrentAttempts(0). Save(ctx) if err != nil { return nil, fmt.Errorf("failed to update session: %w", err) } // Commit transaction if err := tx.Commit(); err != nil { return nil, fmt.Errorf("failed to commit transaction: %w", err) } return &models.GameSession{ ID: session.ID, PlayerName: session.PlayerName, TotalScore: session.TotalScore, StartTime: session.StartTime, Status: string(session.Status), CurrentQuestion: &models.Question{ ID: question.ID, Theme: question.Theme, Text: question.Text, Hint: question.Hint, }, TimeRemaining: s.calculateTimeRemaining(session.StartTime), }, nil } func (s *GameService) validatePlayerName(name string) error { type PlayerNameValidation struct { Name string `validate:"required,min=2,max=50,alphanum_space"` } return s.validator.Struct(&PlayerNameValidation{Name: name}) } func (s *GameService) calculateTimeRemaining(startTime time.Time) int { const sessionDuration = 30 * time.Minute elapsed := time.Since(startTime) remaining := sessionDuration - elapsed if remaining < 0 { return 0 } return int(remaining.Seconds()) } ``` ## TypeScript Development Standards ### React Component Patterns #### Component Structure ```typescript // Component with proper TypeScript types and patterns import React, { useState, useEffect, useCallback } from 'react' import { Card, VStack, HStack, Text, Input, Button, Badge, Progress } from '@gluestack-ui/themed' import { useGameSession } from '../hooks/useGameSession' import { useTimer } from '../hooks/useTimer' // Props interface with proper documentation export interface GameCardProps { /** The question to display */ question: string /** Theme/category of the question */ theme: string /** Time remaining in seconds */ timeRemaining: number /** Number of attempts left */ attemptsLeft: number /** Current player score */ currentScore: number /** Loading state */ isLoading?: boolean /** Callback when answer is submitted */ onSubmitAnswer: (answer: string) => Promise /** Callback when hint is requested */ onRequestHint: () => Promise /** Callback when time expires */ onTimeExpire?: () => void } /** * GameCard component displays a quiz question with input and controls * * @param props - GameCard props * @returns React component */ export const GameCard: React.FC = ({ question, theme, timeRemaining, attemptsLeft, currentScore, isLoading = false, onSubmitAnswer, onRequestHint, onTimeExpire }) => { // Local state with proper typing const [answer, setAnswer] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) // Custom hooks const { formatTime } = useTimer() // Memoized handlers const handleSubmit = useCallback(async () => { if (!answer.trim() || isSubmitting) return setIsSubmitting(true) try { await onSubmitAnswer(answer.trim()) setAnswer('') // Clear input on successful submission } catch (error) { console.error('Failed to submit answer:', error) // Error handling would be managed by parent component } finally { setIsSubmitting(false) } }, [answer, isSubmitting, onSubmitAnswer]) const handleHintRequest = useCallback(async () => { if (isLoading) return try { await onRequestHint() } catch (error) { console.error('Failed to request hint:', error) } }, [isLoading, onRequestHint]) // Effects useEffect(() => { if (timeRemaining === 0 && onTimeExpire) { onTimeExpire() } }, [timeRemaining, onTimeExpire]) // Computed values const progressValue = ((30 * 60 - timeRemaining) / (30 * 60)) * 100 const isTimeWarning = timeRemaining <= 300 // 5 minutes const isTimeCritical = timeRemaining <= 60 // 1 minute return ( {/* Header with theme and timer */} {theme} ⏱️ {formatTime(timeRemaining)} {/* Progress indicator */} {/* Question */} {question} {/* Answer input */} {/* Game stats */} Attempts left: {attemptsLeft}/3 Score: {currentScore} points {/* Action buttons */} ) } // Default export for dynamic imports export default GameCard ``` #### Custom Hooks Pattern ```typescript // Custom hook with proper TypeScript types import { useState, useEffect, useCallback, useRef } from 'react' import { GameSessionService } from '../services/game-session-service' interface UseGameSessionOptions { playerName: string onSessionEnd?: (finalScore: number) => void onError?: (error: Error) => void } interface GameSessionState { sessionId: string | null currentQuestion: Question | null score: number attemptsLeft: number timeRemaining: number isLoading: boolean error: string | null } interface GameSessionActions { startGame: () => Promise submitAnswer: (answer: string) => Promise requestHint: () => Promise endGame: () => Promise } export const useGameSession = ( options: UseGameSessionOptions ): [GameSessionState, GameSessionActions] => { const [state, setState] = useState({ sessionId: null, currentQuestion: null, score: 0, attemptsLeft: 3, timeRemaining: 30 * 60, // 30 minutes isLoading: false, error: null, }) const timerRef = useRef(null) const gameService = useRef(new GameSessionService()) // Timer effect useEffect(() => { if (state.sessionId && state.timeRemaining > 0) { timerRef.current = setInterval(() => { setState(prev => { const newTimeRemaining = prev.timeRemaining - 1 if (newTimeRemaining <= 0 && options.onSessionEnd) { options.onSessionEnd(prev.score) } return { ...prev, timeRemaining: Math.max(0, newTimeRemaining) } }) }, 1000) } return () => { if (timerRef.current) { clearInterval(timerRef.current) } } }, [state.sessionId, state.timeRemaining, options.onSessionEnd]) // Actions const startGame = useCallback(async () => { setState(prev => ({ ...prev, isLoading: true, error: null })) try { const session = await gameService.current.startGame(options.playerName) setState(prev => ({ ...prev, sessionId: session.sessionId, currentQuestion: session.firstQuestion, score: 0, attemptsLeft: 3, timeRemaining: session.timeRemaining, isLoading: false, })) } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Failed to start game' setState(prev => ({ ...prev, error: errorMessage, isLoading: false })) options.onError?.(error instanceof Error ? error : new Error(errorMessage)) } }, [options.playerName, options.onError]) const submitAnswer = useCallback(async (answer: string): Promise => { if (!state.sessionId || !state.currentQuestion) { throw new Error('No active game session') } setState(prev => ({ ...prev, isLoading: true })) try { const result = await gameService.current.submitAnswer( state.sessionId, state.currentQuestion.id, answer ) setState(prev => ({ ...prev, score: result.newScore, attemptsLeft: result.attemptsLeft, currentQuestion: result.nextQuestion, isLoading: false, })) return result.isCorrect } catch (error) { setState(prev => ({ ...prev, isLoading: false })) throw error } }, [state.sessionId, state.currentQuestion]) const requestHint = useCallback(async (): Promise => { if (!state.sessionId || !state.currentQuestion) { throw new Error('No active game session') } const hint = await gameService.current.requestHint( state.sessionId, state.currentQuestion.id ) return hint }, [state.sessionId, state.currentQuestion]) const endGame = useCallback(async () => { if (!state.sessionId) return await gameService.current.endGame(state.sessionId) setState({ sessionId: null, currentQuestion: null, score: 0, attemptsLeft: 3, timeRemaining: 30 * 60, isLoading: false, error: null, }) if (timerRef.current) { clearInterval(timerRef.current) } }, [state.sessionId]) return [ state, { startGame, submitAnswer, requestHint, endGame, }, ] } ``` ## Testing Standards ### Go Testing Patterns ```go // Comprehensive testing with testify and testcontainers package services_test import ( "context" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/testcontainers/testcontainers-go/modules/postgres" "knowfoolery/internal/services" "knowfoolery/ent/enttest" ) type GameServiceTestSuite struct { suite.Suite ctx context.Context gameService *services.GameService client *ent.Client } func (suite *GameServiceTestSuite) SetupSuite() { suite.ctx = context.Background() // Start PostgreSQL container for integration tests postgresContainer, err := postgres.RunContainer(suite.ctx, testcontainers.WithImage("postgres:15-alpine"), postgres.WithDatabase("testdb"), postgres.WithUsername("testuser"), postgres.WithPassword("testpass"), ) require.NoError(suite.T(), err) connStr, err := postgresContainer.ConnectionString(suite.ctx) require.NoError(suite.T(), err) // Create Ent client with test database suite.client = enttest.Open(suite.T(), "postgres", connStr) // Initialize service suite.gameService = services.NewGameService( suite.client, services.NewQuestionService(suite.client), services.NewSessionService(suite.client), ) } func (suite *GameServiceTestSuite) TearDownSuite() { suite.client.Close() } func (suite *GameServiceTestSuite) SetupTest() { // Clean database before each test suite.client.GameSession.Delete().ExecX(suite.ctx) suite.client.Question.Delete().ExecX(suite.ctx) // Seed test data suite.seedTestQuestions() } func (suite *GameServiceTestSuite) TestStartGame_Success() { // Arrange playerName := "TestPlayer" // Act session, err := suite.gameService.StartGame(suite.ctx, "user123", playerName) // Assert require.NoError(suite.T(), err) assert.NotEmpty(suite.T(), session.ID) assert.Equal(suite.T(), playerName, session.PlayerName) assert.Equal(suite.T(), 0, session.TotalScore) assert.NotNil(suite.T(), session.CurrentQuestion) assert.True(suite.T(), session.TimeRemaining > 0) } func (suite *GameServiceTestSuite) TestStartGame_InvalidPlayerName() { // Test cases for validation testCases := []struct { name string playerName string expectedErr string }{ {"empty name", "", "required"}, {"too short", "a", "min"}, {"too long", strings.Repeat("a", 51), "max"}, {"invalid chars", "player@123", "alphanum_space"}, } for _, tc := range testCases { suite.T().Run(tc.name, func(t *testing.T) { _, err := suite.gameService.StartGame(suite.ctx, "user123", tc.playerName) require.Error(t, err) assert.Contains(t, err.Error(), tc.expectedErr) }) } } func (suite *GameServiceTestSuite) TestStartGame_ExistingActiveSession() { // Arrange playerName := "TestPlayer" // Start first game _, err := suite.gameService.StartGame(suite.ctx, "user123", playerName) require.NoError(suite.T(), err) // Act - try to start second game _, err = suite.gameService.StartGame(suite.ctx, "user123", playerName) // Assert require.Error(suite.T(), err) assert.Equal(suite.T(), services.ErrGameInProgress, err) } func (suite *GameServiceTestSuite) seedTestQuestions() { questions := []struct { theme string text string answer string hint string }{ {"Geography", "What is the capital of France?", "Paris", "City of Light"}, {"History", "Who painted the Mona Lisa?", "Leonardo da Vinci", "Renaissance artist"}, {"Science", "What is the largest planet?", "Jupiter", "Gas giant"}, } for _, q := range questions { suite.client.Question.Create(). SetTheme(q.theme). SetText(q.text). SetAnswer(q.answer). SetHint(q.hint). SetIsActive(true). ExecX(suite.ctx) } } func TestGameServiceSuite(t *testing.T) { suite.Run(t, new(GameServiceTestSuite)) } // Benchmark tests func BenchmarkGameService_StartGame(b *testing.B) { // Setup ctx := context.Background() client := enttest.Open(b, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") defer client.Close() gameService := services.NewGameService(client, nil, nil) // Benchmark b.ResetTimer() for i := 0; i < b.N; i++ { playerName := fmt.Sprintf("Player%d", i) _, _ = gameService.StartGame(ctx, fmt.Sprintf("user%d", i), playerName) } } ``` ### TypeScript Testing Patterns ```typescript // React component testing with Jest and Testing Library import React from 'react' import { render, screen, fireEvent, waitFor } from '@testing-library/react' import { GluestackUIProvider } from '@gluestack-ui/themed' import { GameCard, GameCardProps } from '../GameCard' // Test wrapper for Gluestack UI const TestWrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => ( {children} ) // Mock props factory const createMockProps = (overrides: Partial = {}): GameCardProps => ({ question: "What is the capital of France?", theme: "Geography", timeRemaining: 1800, // 30 minutes attemptsLeft: 3, currentScore: 0, onSubmitAnswer: jest.fn(), onRequestHint: jest.fn(), ...overrides, }) describe('GameCard', () => { beforeEach(() => { jest.clearAllMocks() }) describe('Rendering', () => { it('should render question and theme correctly', () => { const props = createMockProps() render(, { wrapper: TestWrapper }) expect(screen.getByTestId('question-text')).toHaveTextContent(props.question) expect(screen.getByTestId('theme-badge')).toHaveTextContent(props.theme) }) it('should display timer in correct format', () => { const props = createMockProps({ timeRemaining: 125 }) // 2:05 render(, { wrapper: TestWrapper }) expect(screen.getByTestId('timer-display')).toHaveTextContent('⏱️ 2:05') }) it('should show attempts and score correctly', () => { const props = createMockProps({ attemptsLeft: 2, currentScore: 5 }) render(, { wrapper: TestWrapper }) expect(screen.getByTestId('attempts-counter')).toHaveTextContent('Attempts left: 2/3') expect(screen.getByTestId('score-display')).toHaveTextContent('Score: 5 points') }) }) describe('User Interactions', () => { it('should call onSubmitAnswer when submit button is clicked', async () => { const props = createMockProps() const mockOnSubmitAnswer = jest.fn().mockResolvedValue(undefined) props.onSubmitAnswer = mockOnSubmitAnswer render(, { wrapper: TestWrapper }) // Type an answer const input = screen.getByTestId('answer-input') fireEvent.changeText(input, 'Paris') // Click submit const submitButton = screen.getByTestId('submit-button') fireEvent.press(submitButton) await waitFor(() => { expect(mockOnSubmitAnswer).toHaveBeenCalledWith('Paris') }) }) it('should not submit empty answer', () => { const props = createMockProps() render(, { wrapper: TestWrapper }) const submitButton = screen.getByTestId('submit-button') expect(submitButton).toBeDisabled() }) it('should call onRequestHint when hint button is clicked', async () => { const props = createMockProps() const mockOnRequestHint = jest.fn().mockResolvedValue('City of Light') props.onRequestHint = mockOnRequestHint render(, { wrapper: TestWrapper }) const hintButton = screen.getByTestId('hint-button') fireEvent.press(hintButton) await waitFor(() => { expect(mockOnRequestHint).toHaveBeenCalled() }) }) }) describe('Loading States', () => { it('should disable inputs when loading', () => { const props = createMockProps({ isLoading: true }) render(, { wrapper: TestWrapper }) expect(screen.getByTestId('answer-input')).toBeDisabled() expect(screen.getByTestId('submit-button')).toBeDisabled() expect(screen.getByTestId('hint-button')).toBeDisabled() }) }) describe('Timer States', () => { it('should show warning color when time is low', () => { const props = createMockProps({ timeRemaining: 300 }) // 5 minutes render(, { wrapper: TestWrapper }) const timer = screen.getByTestId('timer-display') expect(timer).toHaveStyle({ color: expect.stringContaining('warning') }) }) it('should show critical color when time is very low', () => { const props = createMockProps({ timeRemaining: 30 }) // 30 seconds render(, { wrapper: TestWrapper }) const timer = screen.getByTestId('timer-display') expect(timer).toHaveStyle({ color: expect.stringContaining('error') }) }) }) }) // Hook testing import { renderHook, act } from '@testing-library/react' import { useGameSession } from '../hooks/useGameSession' // Mock the service jest.mock('../services/game-session-service') describe('useGameSession', () => { const mockGameService = { startGame: jest.fn(), submitAnswer: jest.fn(), requestHint: jest.fn(), endGame: jest.fn(), } beforeEach(() => { jest.clearAllMocks() // Set up service mock ;(GameSessionService as jest.Mock).mockImplementation(() => mockGameService) }) it('should start game successfully', async () => { mockGameService.startGame.mockResolvedValue({ sessionId: 'session123', firstQuestion: { id: 'q1', text: 'Test question' }, timeRemaining: 1800, }) const { result } = renderHook(() => useGameSession({ playerName: 'TestPlayer' }) ) await act(async () => { await result.current[1].startGame() }) expect(result.current[0].sessionId).toBe('session123') expect(result.current[0].currentQuestion).toBeDefined() }) it('should handle errors during game start', async () => { const mockError = new Error('Failed to start game') mockGameService.startGame.mockRejectedValue(mockError) const onError = jest.fn() const { result } = renderHook(() => useGameSession({ playerName: 'TestPlayer', onError }) ) await act(async () => { await result.current[1].startGame() }) expect(result.current[0].error).toBe('Failed to start game') expect(onError).toHaveBeenCalledWith(mockError) }) }) ``` ## Code Quality Standards ### Linting and Formatting ```yaml # .golangci.yml run: timeout: 5m tests: true linters: enable: - gofmt - goimports - govet - errcheck - staticcheck - unused - gosimple - structcheck - varcheck - ineffassign - deadcode - typecheck - gosec - misspell - lll - unconvert - dupl - goconst - gocyclo linters-settings: lll: line-length: 120 gocyclo: min-complexity: 15 dupl: threshold: 150 issues: exclude-rules: - path: _test\.go linters: - gocyclo - errcheck - dupl - gosec ``` ```json // .eslintrc.json { "extends": [ "@react-native", "@typescript-eslint/recommended", "prettier" ], "parser": "@typescript-eslint/parser", "plugins": ["@typescript-eslint", "react-hooks"], "rules": { "@typescript-eslint/explicit-function-return-type": "error", "@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/prefer-const": "error", "react-hooks/rules-of-hooks": "error", "react-hooks/exhaustive-deps": "warn", "prefer-const": "error", "no-var": "error", "object-shorthand": "error", "prefer-template": "error" } } ``` ### Git Workflow ```bash # Branch naming conventions feature/add-game-session-management bugfix/fix-timer-display-issue hotfix/security-vulnerability-patch refactor/improve-database-queries # Commit message format type(scope): description # Types: feat, fix, docs, style, refactor, test, chore # Examples: feat(game): add session timeout handling fix(auth): resolve JWT token validation issue docs(api): update authentication endpoint documentation test(services): add comprehensive game service tests ``` This comprehensive development guidelines document ensures consistent, maintainable, and high-quality code across the Know Foolery project.