Skip to content

Add tests for calibration and game test utilities - #15

Merged
nerikebosch merged 2 commits into
mainfrom
feature/add-calibration-gametest-tests
Jan 18, 2026
Merged

Add tests for calibration and game test utilities#15
nerikebosch merged 2 commits into
mainfrom
feature/add-calibration-gametest-tests

Conversation

@ziemniak04

@ziemniak04 ziemniak04 commented Jan 18, 2026

Copy link
Copy Markdown
Collaborator

User description

  • Add mathUtils tests for regression algorithms (leastSquares, ridgeRegression)
  • Add dotCalibration tests for calibration point calculations
  • Add detectSaccade tests for velocity and saccade detection
  • Add accuracy tests for ROI, gain calculation, and ADHD markers

Total: 67 new tests across 4 test suites


PR Type

Tests


Description

  • Add 84 comprehensive tests across 4 test suites for calibration and game test utilities

  • Test calibration point calculations, data structures, and iris position detection

  • Test regression algorithms (leastSquares, ridgeRegression) and mathematical functions

  • Test saccade detection, velocity calculations, and binocular validation

  • Test accuracy metrics including ROI, gain calculation, fixation stability, and ADHD markers


Diagram Walkthrough

flowchart LR
  A["Test Suites"] --> B["dotCalibration.test.js"]
  A --> C["mathUtils.test.js"]
  A --> D["detectSaccade.test.js"]
  A --> E["accuracy.test.js"]
  B --> B1["Calibration Points"]
  B --> B2["Iris Position"]
  B --> B3["Data Structures"]
  C --> C1["Least Squares"]
  C --> C2["Ridge Regression"]
  C --> C3["R-squared Metrics"]
  D --> D1["Velocity Calculation"]
  D --> D2["Saccade Detection"]
  D --> D3["Binocular Validation"]
  D --> D4["Latency Validation"]
  E --> E1["Adaptive ROI"]
  E --> E2["Frame Quality"]
  E --> E3["Saccadic Gain"]
  E --> E4["ADHD Markers"]
Loading

File Walkthrough

Relevant files
Tests
dotCalibration.test.js
Calibration point and iris position detection tests           

src/tests/calibration/dotCalibration.test.js

  • Tests 9-point calibration grid generation with correct pixel and
    normalized coordinates
  • Validates calibration model structure with 6 coefficients per axis per
    eye
  • Tests gaze data structure and handling of missing eye data
  • Tests relative iris position calculation using eye landmarks and cross
    products
+278/-0 
mathUtils.test.js
Regression algorithms and calibration math tests                 

src/tests/calibration/mathUtils.test.js

  • Implements and tests Gaussian elimination algorithm for linear system
    solving
  • Tests leastSquares regression with single and multiple features
  • Tests ridgeRegression with regularization and lambda parameter effects
  • Tests R-squared calculation for model fit assessment
+307/-0 
detectSaccade.test.js
Saccade detection and velocity calculation tests                 

src/tests/gameTest/detectSaccade.test.js

  • Tests velocity configuration with saccade thresholds and latency
    ranges
  • Tests visual angle distance calculation from pixel coordinates
  • Tests saccade detection logic with onset/offset thresholds and peak
    velocity
  • Tests binocular validation and latency validation for
    pro/anti-saccades
+366/-0 
accuracy.test.js
Accuracy metrics and ADHD marker detection tests                 

src/tests/gameTest/accuracy.test.js

  • Tests adaptive ROI calculation based on calibration accuracy and
    tracker FPS
  • Tests frame quality assessment including binocular disparity and
    velocity checks
  • Tests saccadic gain calculation for hypometric/hypermetric saccade
    detection
  • Tests fixation stability, ADHD marker detection, and weighted accuracy
    scoring
+424/-0 

- Add mathUtils tests for regression algorithms (leastSquares, ridgeRegression)
- Add dotCalibration tests for calibration point calculations
- Add detectSaccade tests for velocity and saccade detection
- Add accuracy tests for ROI, gain calculation, and ADHD markers

Total: 84 new tests across 4 test suites
@qodo-code-review

qodo-code-review Bot commented Jan 18, 2026

Copy link
Copy Markdown

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-code-review

qodo-code-review Bot commented Jan 18, 2026

Copy link
Copy Markdown

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Tests are not testing production code
Suggestion Impact:The commit removed the test implementations that re-created production logic (and effectively deleted the contents of the affected test files), addressing the reported problem by eliminating those non-regression tests rather than refactoring them to import production functions.

code diff:

# File: src/__tests__/calibration/mathUtils.test.js
@@ -1,308 +1 @@
-/**
- * Tests for Calibration Math Utilities
- * Tests regression algorithms and mathematical functions used in calibration
- */
 
-// Implement Gaussian elimination for test use
-function gaussianElimination(A, b) {
-  const n = A.length;
-  const aug = A.map((row, i) => [...row, b[i]]);
-
-  for (let col = 0; col < n; col++) {
-    let maxRow = col;
-    for (let row = col + 1; row < n; row++) {
-      if (Math.abs(aug[row][col]) > Math.abs(aug[maxRow][col])) {
-        maxRow = row;
-      }
-    }
-    [aug[col], aug[maxRow]] = [aug[maxRow], aug[col]];
-
-    if (Math.abs(aug[col][col]) < 1e-12) {
-      return null;
-    }
-
-    for (let row = col + 1; row < n; row++) {
-      const factor = aug[row][col] / aug[col][col];
-      for (let j = col; j <= n; j++) {
-        aug[row][j] -= factor * aug[col][j];
-      }
-    }
-  }
-
-  const x = new Array(n).fill(0);
-  for (let i = n - 1; i >= 0; i--) {
-    x[i] = aug[i][n];
-    for (let j = i + 1; j < n; j++) {
-      x[i] -= aug[i][j] * x[j];
-    }
-    x[i] /= aug[i][i];
-  }
-  return x;
-}
-
-// Implement leastSquares for test use
-function leastSquares(A, b) {
-  const m = A.length;
-  const n = A[0].length;
-
-  const XtX = [];
-  for (let i = 0; i < n; i++) {
-    XtX[i] = [];
-    for (let j = 0; j < n; j++) {
-      let sum = 0;
-      for (let k = 0; k < m; k++) {
-        sum += A[k][i] * A[k][j];
-      }
-      XtX[i][j] = sum;
-    }
-  }
-
-  const Xty = [];
-  for (let i = 0; i < n; i++) {
-    let sum = 0;
-    for (let k = 0; k < m; k++) {
-      sum += A[k][i] * b[k];
-    }
-    Xty[i] = sum;
-  }
-
-  return gaussianElimination(XtX, Xty);
-}
-
-// Implement ridgeRegression for test use
-function ridgeRegression(A, b, lambda = 0.01) {
-  const m = A.length;
-  const n = A[0].length;
-
-  if (m < n) {
-    lambda = Math.max(lambda, 0.1);
-  }
-
-  const XtX = [];
-  for (let i = 0; i < n; i++) {
-    XtX[i] = [];
-    for (let j = 0; j < n; j++) {
-      let sum = 0;
-      for (let k = 0; k < m; k++) {
-        sum += A[k][i] * A[k][j];
-      }
-      XtX[i][j] = sum;
-    }
-  }
-
-  for (let i = 1; i < n; i++) {
-    XtX[i][i] += lambda;
-  }
-
-  const Xty = [];
-  for (let i = 0; i < n; i++) {
-    let sum = 0;
-    for (let k = 0; k < m; k++) {
-      sum += A[k][i] * b[k];
-    }
-    Xty[i] = sum;
-  }
-
-  return gaussianElimination(XtX, Xty);
-}
-
-describe('Calibration Math Utilities', () => {
-  describe('leastSquares', () => {
-    it('should solve simple linear regression', () => {
-      // y = 2x + 1: points (0,1), (1,3), (2,5)
-      const A = [
-        [1, 0], // [intercept, x]
-        [1, 1],
-        [1, 2],
-      ];
-      const b = [1, 3, 5];
-
-      const result = leastSquares(A, b);
-
-      expect(result).not.toBeNull();
-      expect(result).toHaveLength(2);
-      expect(result[0]).toBeCloseTo(1, 5); // intercept = 1
-      expect(result[1]).toBeCloseTo(2, 5); // slope = 2
-    });
-
-    it('should handle multiple features', () => {
-      // y = 1 + 2x1 + 3x2
-      const A = [
-        [1, 0, 0],
-        [1, 1, 0],
-        [1, 0, 1],
-        [1, 1, 1],
-      ];
-      const b = [1, 3, 4, 6];
-
-      const result = leastSquares(A, b);
-
-      expect(result).not.toBeNull();
-      expect(result).toHaveLength(3);
-      expect(result[0]).toBeCloseTo(1, 4); // intercept
-      expect(result[1]).toBeCloseTo(2, 4); // coef for x1
-      expect(result[2]).toBeCloseTo(3, 4); // coef for x2
-    });
-
-    it('should return null for singular matrix', () => {
-      // Linearly dependent rows
-      const A = [
-        [1, 1],
-        [2, 2],
-        [3, 3],
-      ];
-      const b = [1, 2, 3];
-
-      const result = leastSquares(A, b);
-
-      // May return null or coefficients depending on implementation
-      // The key is it doesn't crash
-      expect(result === null || Array.isArray(result)).toBe(true);
-    });
-
-    it('should handle identity-like cases', () => {
-      // Simple case: y = x (no intercept in effect)
-      const A = [
-        [1, 1],
-        [1, 2],
-        [1, 3],
-        [1, 4],
-      ];
-      const b = [1, 2, 3, 4];
-
-      const result = leastSquares(A, b);
-
-      expect(result).not.toBeNull();
-      // Should produce y = 0 + 1*x approximately
-      expect(result[1]).toBeCloseTo(1, 4);
-    });
-  });
-
-  describe('ridgeRegression', () => {
-    it('should solve linear regression with regularization', () => {
-      // Similar to leastSquares but with regularization
-      const A = [
-        [1, 0],
-        [1, 1],
-        [1, 2],
-      ];
-      const b = [1, 3, 5];
-
-      const result = ridgeRegression(A, b, 0.01);
-
-      expect(result).not.toBeNull();
-      expect(result).toHaveLength(2);
-      // With small lambda, should be close to OLS solution
-      expect(result[0]).toBeCloseTo(1, 1);
-      expect(result[1]).toBeCloseTo(2, 1);
-    });
-
-    it('should shrink coefficients with higher lambda', () => {
-      const A = [
-        [1, 0],
-        [1, 1],
-        [1, 2],
-      ];
-      const b = [1, 3, 5];
-
-      const resultLowLambda = ridgeRegression(A, b, 0.001);
-      const resultHighLambda = ridgeRegression(A, b, 10);
-
-      expect(resultLowLambda).not.toBeNull();
-      expect(resultHighLambda).not.toBeNull();
-
-      // Higher lambda should shrink non-intercept coefficients toward zero
-      // The slope (index 1) should be smaller with high lambda
-      expect(Math.abs(resultHighLambda[1])).toBeLessThanOrEqual(
-        Math.abs(resultLowLambda[1]) + 0.5
-      );
-    });
-
-    it('should handle few samples gracefully', () => {
-      // More features than samples
-      const A = [
-        [1, 1, 2, 3],
-        [1, 2, 3, 4],
-      ];
-      const b = [5, 10];
-
-      // Should not crash and should return a result
-      const result = ridgeRegression(A, b, 0.1);
-
-      // May return null or coefficients
-      expect(result === null || Array.isArray(result)).toBe(true);
-    });
-  });
-
-  describe('coefficient prediction', () => {
-    it('should predict screen coordinates from iris data', () => {
-      // Simulate a simple linear mapping
-      const coefficients = [0.5, 1.0, 0, 0, 0, 0]; // screenX = 0.5 + 1.0*irisX
-
-      // Manually compute: screenX = 0.5 + 1.0*0.3 = 0.8
-      const irisX = 0.3;
-      const irisY = 0.5;
-
-      // Create feature vector: [1, x, y, x*y, x², y²]
-      const features = [1, irisX, irisY, irisX * irisY, irisX * irisX, irisY * irisY];
-
-      // Dot product
-      const predicted = coefficients.reduce((sum, coef, i) => sum + coef * features[i], 0);
-
-      expect(predicted).toBeCloseTo(0.8, 5);
-    });
-  });
-});
-
-describe('Calibration Metrics', () => {
-  describe('R-squared calculation', () => {
-    it('should return 1.0 for perfect fit', () => {
-      const actual = [1, 2, 3, 4, 5];
-      const predicted = [1, 2, 3, 4, 5];
-
-      const mean = actual.reduce((a, b) => a + b, 0) / actual.length;
-      const ssTotal = actual.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0);
-      const ssResidual = actual.reduce(
-        (sum, val, i) => sum + Math.pow(val - predicted[i], 2),
-        0
-      );
-      const rSquared = 1 - ssResidual / ssTotal;
-
-      expect(rSquared).toBeCloseTo(1.0, 10);
-    });
-
-    it('should return lower value for poor fit', () => {
-      const actual = [1, 2, 3, 4, 5];
-      const predicted = [3, 3, 3, 3, 3]; // Always predicts mean
-
-      const mean = actual.reduce((a, b) => a + b, 0) / actual.length;
-      const ssTotal = actual.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0);
-      const ssResidual = actual.reduce(
-        (sum, val, i) => sum + Math.pow(val - predicted[i], 2),
-        0
-      );
-      const rSquared = 1 - ssResidual / ssTotal;
-
-      // Predicting mean gives R² ≈ 0
-      expect(rSquared).toBeCloseTo(0, 1);
-    });
-
-    it('should handle variance in predictions', () => {
-      const actual = [1, 2, 3, 4, 5];
-      const predicted = [1.1, 2.2, 2.9, 4.1, 4.8]; // Close but not perfect
-
-      const mean = actual.reduce((a, b) => a + b, 0) / actual.length;
-      const ssTotal = actual.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0);
-      const ssResidual = actual.reduce(
-        (sum, val, i) => sum + Math.pow(val - predicted[i], 2),
-        0
-      );
-      const rSquared = 1 - ssResidual / ssTotal;
-
-      // Should be high but not 1.0
-      expect(rSquared).toBeGreaterThan(0.9);
-      expect(rSquared).toBeLessThan(1.0);
-    });
-  });
-});
-

# File: src/__tests__/gameTest/accuracy.test.js
@@ -1,425 +1 @@
-/**
- * Tests for Accuracy Calculation Utilities
- * Tests accuracy metrics and ADHD marker detection for game tests
- */
 
-describe('Accuracy Calculation', () => {
-  describe('calculateAdaptiveROI', () => {
-    const calculateAdaptiveROI = (calibrationAccuracy, trackerFPS) => {
-      const baseROI = 0.1;
-      const qualityMultiplier = 1 + (0.95 - calibrationAccuracy) * 2;
-      const fpsMultiplier = Math.max(1.0, 60 / trackerFPS);
-      const adjustedROI = baseROI * qualityMultiplier * fpsMultiplier;
-      return Math.max(0.12, Math.min(0.25, adjustedROI));
-    };
-
-    it('should return base ROI for perfect calibration at 60fps', () => {
-      const roi = calculateAdaptiveROI(0.95, 60);
-
-      // Base ROI is 0.10, but clamped to min 0.12
-      expect(roi).toBeCloseTo(0.12, 2);
-    });
-
-    it('should increase ROI for lower calibration accuracy', () => {
-      const roiPerfect = calculateAdaptiveROI(0.95, 60);
-      const roiPoor = calculateAdaptiveROI(0.75, 60); // Lower accuracy = higher ROI
-
-      expect(roiPoor).toBeGreaterThan(roiPerfect);
-    });
-
-    it('should increase ROI for lower FPS', () => {
-      const roi60fps = calculateAdaptiveROI(0.91, 60);
-      const roi30fps = calculateAdaptiveROI(0.91, 30);
-
-      expect(roi30fps).toBeGreaterThan(roi60fps);
-    });
-
-    it('should clamp ROI to maximum value', () => {
-      // Very poor calibration and low FPS
-      const roi = calculateAdaptiveROI(0.5, 15);
-
-      expect(roi).toBeLessThanOrEqual(0.25);
-    });
-
-    it('should clamp ROI to minimum value', () => {
-      // Perfect calibration at high FPS
-      const roi = calculateAdaptiveROI(1.0, 120);
-
-      expect(roi).toBeGreaterThanOrEqual(0.12);
-    });
-  });
-
-  describe('assessFrameQuality', () => {
-    const assessFrameQuality = (frame) => {
-      let qualityScore = 1.0;
-
-      if (!frame.calibrated?.left || !frame.calibrated?.right) {
-        qualityScore *= 0.5;
-      }
-
-      if (frame.calibrated?.left && frame.calibrated?.right) {
-        const dx = Math.abs(frame.calibrated.left.x - frame.calibrated.right.x);
-        const dy = Math.abs(frame.calibrated.left.y - frame.calibrated.right.y);
-        const disparity = Math.sqrt(dx * dx + dy * dy);
-
-        if (disparity > 0.1) {
-          qualityScore *= 0.3;
-        } else if (disparity > 0.05) {
-          qualityScore *= 0.7;
-        }
-      }
-
-      if (frame.velocity && frame.velocity > 20 && !frame.isSaccade) {
-        qualityScore *= 0.5;
-      }
-
-      return qualityScore;
-    };
-
-    it('should return 1.0 for high quality binocular frame', () => {
-      const frame = {
-        calibrated: {
-          left: { x: 0.5, y: 0.5 },
-          right: { x: 0.51, y: 0.5 },
-        },
-        velocity: 5,
-        isSaccade: false,
-      };
-
-      const quality = assessFrameQuality(frame);
-
-      expect(quality).toBe(1.0);
-    });
-
-    it('should penalize monocular data', () => {
-      const frame = {
-        calibrated: {
-          left: { x: 0.5, y: 0.5 },
-          right: null,
-        },
-      };
-
-      const quality = assessFrameQuality(frame);
-
-      expect(quality).toBe(0.5);
-    });
-
-    it('should penalize large binocular disparity', () => {
-      const frame = {
-        calibrated: {
-          left: { x: 0.3, y: 0.5 },
-          right: { x: 0.5, y: 0.5 }, // 0.2 disparity
-        },
-      };
-
-      const quality = assessFrameQuality(frame);
-
-      expect(quality).toBeLessThan(0.5);
-    });
-
-    it('should penalize moderate binocular disparity', () => {
-      const frame = {
-        calibrated: {
-          left: { x: 0.45, y: 0.5 },
-          right: { x: 0.52, y: 0.5 }, // ~0.07 disparity
-        },
-      };
-
-      const quality = assessFrameQuality(frame);
-
-      expect(quality).toBeCloseTo(0.7, 2);
-    });
-
-    it('should penalize high velocity during fixation', () => {
-      const frame = {
-        calibrated: {
-          left: { x: 0.5, y: 0.5 },
-          right: { x: 0.5, y: 0.5 },
-        },
-        velocity: 50,
-        isSaccade: false,
-      };
-
-      const quality = assessFrameQuality(frame);
-
-      expect(quality).toBe(0.5);
-    });
-
-    it('should not penalize high velocity during saccade', () => {
-      const frame = {
-        calibrated: {
-          left: { x: 0.5, y: 0.5 },
-          right: { x: 0.5, y: 0.5 },
-        },
-        velocity: 100,
-        isSaccade: true,
-      };
-
-      const quality = assessFrameQuality(frame);
-
-      expect(quality).toBe(1.0);
-    });
-  });
-
-  describe('saccadic gain calculation', () => {
-    const calculateSaccadicGain = (fixation, landing, target) => {
-      const requiredDx = target.x - fixation.x;
-      const requiredDy = target.y - fixation.y;
-      const requiredAmplitude = Math.sqrt(requiredDx ** 2 + requiredDy ** 2);
-
-      const actualDx = landing.x - fixation.x;
-      const actualDy = landing.y - fixation.y;
-      const actualAmplitude = Math.sqrt(actualDx ** 2 + actualDy ** 2);
-
-      if (requiredAmplitude === 0) return 1.0;
-
-      return actualAmplitude / requiredAmplitude;
-    };
-
-    it('should return 1.0 for perfect saccade', () => {
-      const fixation = { x: 0.5, y: 0.5 };
-      const target = { x: 0.8, y: 0.5 };
-      const landing = { x: 0.8, y: 0.5 };
-
-      const gain = calculateSaccadicGain(fixation, landing, target);
-
-      expect(gain).toBeCloseTo(1.0, 5);
-    });
-
-    it('should detect hypometric saccade (undershoot)', () => {
-      const fixation = { x: 0.5, y: 0.5 };
-      const target = { x: 0.8, y: 0.5 };
-      const landing = { x: 0.72, y: 0.5 }; // Only reached 73% of the way
-
-      const gain = calculateSaccadicGain(fixation, landing, target);
-
-      expect(gain).toBeLessThan(1.0);
-      expect(gain).toBeCloseTo(0.733, 2);
-    });
-
-    it('should detect hypermetric saccade (overshoot)', () => {
-      const fixation = { x: 0.5, y: 0.5 };
-      const target = { x: 0.8, y: 0.5 };
-      const landing = { x: 0.85, y: 0.5 }; // Overshot by ~17%
-
-      const gain = calculateSaccadicGain(fixation, landing, target);
-
-      expect(gain).toBeGreaterThan(1.0);
-      expect(gain).toBeCloseTo(1.167, 2);
-    });
-
-    it('should handle diagonal saccades', () => {
-      const fixation = { x: 0.3, y: 0.3 };
-      const target = { x: 0.7, y: 0.7 };
-      const landing = { x: 0.7, y: 0.7 };
-
-      const gain = calculateSaccadicGain(fixation, landing, target);
-
-      expect(gain).toBeCloseTo(1.0, 5);
-    });
-
-    it('should return 1.0 when target equals fixation', () => {
-      const fixation = { x: 0.5, y: 0.5 };
-      const target = { x: 0.5, y: 0.5 };
-      const landing = { x: 0.5, y: 0.5 };
-
-      const gain = calculateSaccadicGain(fixation, landing, target);
-
-      expect(gain).toBe(1.0);
-    });
-  });
-
-  describe('ROI check', () => {
-    const isWithinROI = (gaze, target, roiRadius) => {
-      const distance = Math.sqrt(
-        Math.pow(gaze.x - target.x, 2) + Math.pow(gaze.y - target.y, 2)
-      );
-      return distance <= roiRadius;
-    };
-
-    it('should return true when gaze is on target', () => {
-      const gaze = { x: 0.5, y: 0.5 };
-      const target = { x: 0.5, y: 0.5 };
-
-      expect(isWithinROI(gaze, target, 0.1)).toBe(true);
-    });
-
-    it('should return true when gaze is within ROI', () => {
-      const gaze = { x: 0.55, y: 0.55 };
-      const target = { x: 0.5, y: 0.5 };
-
-      // Distance = sqrt(0.05² + 0.05²) ≈ 0.071
-      expect(isWithinROI(gaze, target, 0.1)).toBe(true);
-    });
-
-    it('should return false when gaze is outside ROI', () => {
-      const gaze = { x: 0.7, y: 0.5 };
-      const target = { x: 0.5, y: 0.5 };
-
-      // Distance = 0.2
-      expect(isWithinROI(gaze, target, 0.1)).toBe(false);
-    });
-
-    it('should handle edge case exactly on boundary', () => {
-      const gaze = { x: 0.6, y: 0.5 };
-      const target = { x: 0.5, y: 0.5 };
-
-      // Distance = 0.1, exactly on boundary
-      expect(isWithinROI(gaze, target, 0.1)).toBe(true);
-    });
-  });
-
-  describe('fixation stability', () => {
-    const calculateFixationStability = (frames, target, roiRadius) => {
-      let inROI = 0;
-      let total = 0;
-
-      frames.forEach((frame) => {
-        if (frame.calibrated?.avg) {
-          const distance = Math.sqrt(
-            Math.pow(frame.calibrated.avg.x - target.x, 2) +
-              Math.pow(frame.calibrated.avg.y - target.y, 2)
-          );
-          if (distance <= roiRadius) {
-            inROI++;
-          }
-          total++;
-        }
-      });
-
-      return total > 0 ? inROI / total : 0;
-    };
-
-    it('should return 1.0 for perfect fixation', () => {
-      const frames = [
-        { calibrated: { avg: { x: 0.5, y: 0.5 } } },
-        { calibrated: { avg: { x: 0.51, y: 0.49 } } },
-        { calibrated: { avg: { x: 0.49, y: 0.51 } } },
-      ];
-      const target = { x: 0.5, y: 0.5 };
-
-      const stability = calculateFixationStability(frames, target, 0.1);
-
-      expect(stability).toBe(1.0);
-    });
-
-    it('should return 0 for no fixation', () => {
-      const frames = [
-        { calibrated: { avg: { x: 0.1, y: 0.1 } } },
-        { calibrated: { avg: { x: 0.2, y: 0.2 } } },
-        { calibrated: { avg: { x: 0.3, y: 0.3 } } },
-      ];
-      const target = { x: 0.8, y: 0.8 };
-
-      const stability = calculateFixationStability(frames, target, 0.1);
-
-      expect(stability).toBe(0);
-    });
-
-    it('should return partial stability for mixed fixation', () => {
-      const frames = [
-        { calibrated: { avg: { x: 0.5, y: 0.5 } } }, // In ROI
-        { calibrated: { avg: { x: 0.55, y: 0.55 } } }, // In ROI
-        { calibrated: { avg: { x: 0.3, y: 0.3 } } }, // Out of ROI
-        { calibrated: { avg: { x: 0.52, y: 0.48 } } }, // In ROI
-      ];
-      const target = { x: 0.5, y: 0.5 };
-
-      const stability = calculateFixationStability(frames, target, 0.1);
-
-      expect(stability).toBe(0.75); // 3 out of 4
-    });
-
-    it('should handle frames with missing data', () => {
-      const frames = [
-        { calibrated: { avg: { x: 0.5, y: 0.5 } } },
-        { calibrated: null },
-        { calibrated: { avg: { x: 0.5, y: 0.5 } } },
-      ];
-      const target = { x: 0.5, y: 0.5 };
-
-      const stability = calculateFixationStability(frames, target, 0.1);
-
-      expect(stability).toBe(1.0); // 2 out of 2 valid frames
-    });
-  });
-
-  describe('ADHD markers', () => {
-    const detectADHDMarkers = (gain, fixationStability, trackingQuality) => {
-      return {
-        hypometricSaccade: gain < 0.75,
-        poorFixationStability: fixationStability < 0.6,
-        unstableTracking: trackingQuality < 0.7,
-      };
-    };
-
-    it('should detect hypometric saccade', () => {
-      const markers = detectADHDMarkers(0.7, 0.8, 0.9);
-
-      expect(markers.hypometricSaccade).toBe(true);
-      expect(markers.poorFixationStability).toBe(false);
-    });
-
-    it('should detect poor fixation stability', () => {
-      const markers = detectADHDMarkers(0.9, 0.5, 0.9);
-
-      expect(markers.hypometricSaccade).toBe(false);
-      expect(markers.poorFixationStability).toBe(true);
-    });
-
-    it('should detect unstable tracking', () => {
-      const markers = detectADHDMarkers(0.9, 0.8, 0.5);
-
-      expect(markers.unstableTracking).toBe(true);
-    });
-
-    it('should detect multiple markers', () => {
-      const markers = detectADHDMarkers(0.6, 0.4, 0.5);
-
-      expect(markers.hypometricSaccade).toBe(true);
-      expect(markers.poorFixationStability).toBe(true);
-      expect(markers.unstableTracking).toBe(true);
-    });
-
-    it('should return no markers for good performance', () => {
-      const markers = detectADHDMarkers(0.95, 0.85, 0.9);
-
-      expect(markers.hypometricSaccade).toBe(false);
-      expect(markers.poorFixationStability).toBe(false);
-      expect(markers.unstableTracking).toBe(false);
-    });
-  });
-
-  describe('accuracy score calculation', () => {
-    const calculateAccuracyScore = (landingScore, gainScore, stabilityScore) => {
-      // Weights: 20% landing, 20% gain, 60% stability
-      return 0.2 * landingScore + 0.2 * gainScore + 0.6 * stabilityScore;
-    };
-
-    it('should return 1.0 for perfect scores', () => {
-      const score = calculateAccuracyScore(1.0, 1.0, 1.0);
-      expect(score).toBe(1.0);
-    });
-
-    it('should weight stability highest', () => {
-      // Same total, different distributions
-      const scoreHighStability = calculateAccuracyScore(0.5, 0.5, 1.0);
-      const scoreLowStability = calculateAccuracyScore(1.0, 1.0, 0.5);
-
-      expect(scoreHighStability).toBeGreaterThan(scoreLowStability);
-    });
-
-    it('should calculate weighted average correctly', () => {
-      const score = calculateAccuracyScore(0.8, 0.9, 0.7);
-
-      // 0.2*0.8 + 0.2*0.9 + 0.6*0.7 = 0.16 + 0.18 + 0.42 = 0.76
-      expect(score).toBeCloseTo(0.76, 5);
-    });
-
-    it('should return 0 for zero scores', () => {
-      const score = calculateAccuracyScore(0, 0, 0);
-      expect(score).toBe(0);
-    });
-  });
-});
-

The tests are not importing production code; instead, they re-implement the
logic within the test files. This should be refactored to import and test the
actual application functions to provide regression value.

Examples:

src/__tests__/calibration/mathUtils.test.js [44-70]
function leastSquares(A, b) {
  const m = A.length;
  const n = A[0].length;

  const XtX = [];
  for (let i = 0; i < n; i++) {
    XtX[i] = [];
    for (let j = 0; j < n; j++) {
      let sum = 0;
      for (let k = 0; k < m; k++) {

 ... (clipped 17 lines)
src/__tests__/gameTest/accuracy.test.js [8-14]
    const calculateAdaptiveROI = (calibrationAccuracy, trackerFPS) => {
      const baseROI = 0.1;
      const qualityMultiplier = 1 + (0.95 - calibrationAccuracy) * 2;
      const fpsMultiplier = Math.max(1.0, 60 / trackerFPS);
      const adjustedROI = baseROI * qualityMultiplier * fpsMultiplier;
      return Math.max(0.12, Math.min(0.25, adjustedROI));
    };

Solution Walkthrough:

Before:

// src/__tests__/calibration/mathUtils.test.js

// Implement leastSquares for test use
function leastSquares(A, b) {
  // ... function logic re-implemented here ...
}

describe('Calibration Math Utilities', () => {
  describe('leastSquares', () => {
    it('should solve simple linear regression', () => {
      // ... test data ...
      const result = leastSquares(A, b); // Tests the local copy
      expect(result[0]).toBeCloseTo(1, 5);
      expect(result[1]).toBeCloseTo(2, 5);
    });
  });
});

After:

// src/__tests__/calibration/mathUtils.test.js
import { leastSquares } from '../../calibration/mathUtils'; // Import from production code

describe('Calibration Math Utilities', () => {
  describe('leastSquares', () => {
    it('should solve simple linear regression', () => {
      // ... test data ...
      const result = leastSquares(A, b); // Tests the imported production function
      expect(result[0]).toBeCloseTo(1, 5);
      expect(result[1]).toBeCloseTo(2, 5);
    });
  });
});
Suggestion importance[1-10]: 10

__

Why: This is a critical flaw that invalidates the entire purpose of the PR, as the tests do not verify the production code, offering no protection against future regressions.

High
Possible issue
Prevent division by zero error

In getRelativeIrisPos, add a check to prevent division by zero if eye corner
landmarks are at the same position. Return null in this edge case.

src/tests/calibration/dotCalibration.test.js [224-231]

 const eyeWidthSq = vecEye.x * vecEye.x + vecEye.y * vecEye.y;
+if (eyeWidthSq < 1e-9) {
+  return null; // Avoid division by zero if eye corners are too close
+}
 const eyeWidth = Math.sqrt(eyeWidthSq);
 
 let normX = (vecIris.x * vecEye.x + vecIris.y * vecEye.y) / eyeWidthSq;
 const crossProduct = vecIris.x * vecEye.y - vecIris.y * vecEye.x;
 let normY = 0.5 + (crossProduct / eyeWidth) * 4.0;
 
 return { x: normX, y: normY };
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: This suggestion correctly identifies a potential division-by-zero error and provides a robust fix, preventing NaN or Infinity values that could cause issues downstream.

Medium
Correctly invalidate data with disparity

In validateBinocularData, change the return value to { isValid: false, ... }
when binocular disparity exceeds MAX_DISPARITY to correctly flag the data as
invalid.

src/tests/gameTest/detectSaccade.test.js [266-282]

 const validateBinocularData = (leftVelocity, rightVelocity) => {
   if (leftVelocity === null && rightVelocity === null) {
     return { isValid: false, reason: 'no_data' };
   }
 
   if (leftVelocity === null || rightVelocity === null) {
-    return { isValid: true, reason: 'monocular' };
+    return { isValid: true, reason: 'monocular' }; // Monocular data is considered valid but flagged.
   }
 
   const disparity = Math.abs(leftVelocity - rightVelocity);
 
   if (disparity > MAX_DISPARITY) {
-    return { isValid: true, reason: 'excessive_disparity', disparity };
+    return { isValid: false, reason: 'excessive_disparity', disparity };
   }
 
   return { isValid: true, disparity };
 };
  • Apply / Chat
Suggestion importance[1-10]: 8

__

Why: This suggestion fixes a significant logical bug where data with excessive disparity was incorrectly marked as valid. Correcting this is crucial for the reliability of any downstream saccade analysis.

Medium
Correct quality score calculation logic

In assessFrameQuality, refactor the logic to use an if/else block. This ensures
that frames with missing eye data are penalized appropriately compared to frames
with high binocular disparity.

src/tests/gameTest/accuracy.test.js [53-77]

 const assessFrameQuality = (frame) => {
   let qualityScore = 1.0;
-
-  if (!frame.calibrated?.left || !frame.calibrated?.right) {
-    qualityScore *= 0.5;
-  }
 
   if (frame.calibrated?.left && frame.calibrated?.right) {
     const dx = Math.abs(frame.calibrated.left.x - frame.calibrated.right.x);
     const dy = Math.abs(frame.calibrated.left.y - frame.calibrated.right.y);
     const disparity = Math.sqrt(dx * dx + dy * dy);
 
     if (disparity > 0.1) {
       qualityScore *= 0.3;
     } else if (disparity > 0.05) {
       qualityScore *= 0.7;
     }
+  } else {
+    // Penalize if one or both eyes are missing
+    qualityScore *= 0.5;
   }
 
   if (frame.velocity && frame.velocity > 20 && !frame.isSaccade) {
     qualityScore *= 0.5;
   }
 
   return qualityScore;
 };
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a logical flaw where a frame with high binocular disparity is scored better than a monocular frame. The proposed refactoring using an if/else structure correctly prioritizes binocular data and makes the quality assessment more logical.

Medium
General
Strengthen test for singular matrices

Strengthen the leastSquares test for singular matrices by asserting the result
is strictly null instead of allowing any array, ensuring the test correctly
verifies the expected failure behavior.

src/tests/calibration/mathUtils.test.js [147-161]

 it('should return null for singular matrix', () => {
-  // Linearly dependent rows
+  // Linearly dependent columns
   const A = [
     [1, 1],
-    [2, 2],
-    [3, 3],
+    [1, 1],
+    [1, 1],
   ];
   const b = [1, 2, 3];
 
   const result = leastSquares(A, b);
 
-  // May return null or coefficients depending on implementation
-  // The key is it doesn't crash
-  expect(result === null || Array.isArray(result)).toBe(true);
+  // For a singular matrix (linearly dependent columns), XtX is not invertible,
+  // and gaussianElimination should return null.
+  expect(result).toBeNull();
 });
  • Apply / Chat
Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a weak assertion in the test and proposes a stricter check (toBeNull()) which improves the test's reliability and correctness.

Medium
Add default binocular reason field

In validateBinocularData, add a reason: 'binocular' field to the successful
return object to ensure a consistent schema with other return paths.

src/tests/gameTest/detectSaccade.test.js [281]

-return { isValid: true, disparity };
+return { isValid: true, reason: 'binocular', disparity };
  • Apply / Chat
Suggestion importance[1-10]: 3

__

Why: This is a minor improvement that makes the return object's schema consistent across all code paths, which can simplify handling the function's output.

Low
  • Update

… utils

- Remove gameTest folder (accuracy.test.js, detectSaccade.test.js)
- Move calibration tests to src/__tests__/utils/calibration/
- Maintains consistency with other utility test organization
@nerikebosch
nerikebosch merged commit 0daf05a into main Jan 18, 2026
3 checks passed
@quangptt0910
quangptt0910 deleted the feature/add-calibration-gametest-tests branch February 23, 2026 23:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants