Skip to main content
Off Grid has comprehensive test coverage across React Native, Android, iOS, and end-to-end flows. All tests run automatically on every pull request via GitHub Actions.

Test Frameworks

Tests run across three platforms with specialized frameworks:
PlatformFrameworkCoverage
React NativeJest + React Native Testing LibraryStores, services, components, screens, contracts
AndroidJUnitLocalDream, DownloadManager, BroadcastReceiver
iOSXCTestPDFExtractor, CoreMLDiffusion, DownloadManager
E2EMaestroCritical path flows (launch, chat, models, downloads)

Running Tests

All Tests

Run the complete test suite (Jest + Android + iOS):
npm test
This command runs:
  1. Jest tests with coverage
  2. Android JUnit tests
  3. iOS XCTest suite

React Native Tests (Jest)

npx jest --coverage --forceExit

Android Tests (JUnit)

npm run test:android
Android tests require the Gradle wrapper in android/. Ensure you’ve run cd android && ./gradlew clean at least once.

iOS Tests (XCTest)

npm run test:ios
iOS tests require Xcode 15+ and a configured iOS Simulator. The default simulator is iPhone 16 - adjust the -destination flag if using a different simulator.

End-to-End Tests (Maestro)

Maestro tests verify critical user flows end-to-end.
1

Install Maestro

curl -Ls "https://get.maestro.mobile.dev" | bash
2

Start the app

Launch the app on an emulator/simulator:
npm run android
3

Run E2E tests

npm run test:e2e
# or
./scripts/run-tests.sh
E2E tests require a running app on an emulator/simulator. They test:
  • App launch and onboarding
  • Chat message flow
  • Model download and switching
  • Image generation
  • Settings changes

Test Coverage

Off Grid uses Codecov for test coverage tracking: codecov

Viewing Coverage Reports

After running Jest with --coverage, view the HTML report:
open coverage/lcov-report/index.html

Coverage Requirements

  • Jest coverage is uploaded to Codecov on every PR
  • Codecov checks enforce coverage thresholds (no decrease in coverage allowed)
  • Uncovered lines are flagged in PR comments
Add tests for new code before submitting PRs. The CI pipeline will reject PRs that decrease test coverage.

Continuous Integration (GitHub Actions)

All tests run automatically on every PR and push to main via GitHub Actions.

CI Workflow Jobs

The CI workflow (.github/workflows/ci.yml) includes:
Job: lintRuns on: macos-latestSteps:
  1. ESLint (JavaScript/TypeScript)
  2. SwiftLint (iOS Swift code)
  3. Android Lint (Kotlin code)
npm run lint
This runs:
  • eslint .
  • swiftlint lint --quiet
  • cd android && ./gradlew :app:lintDebug

CI Badge

CI The CI badge shows the current status of the main branch.

Pre-Commit Quality Gates (Husky)

Off Grid uses Husky to run quality gates automatically on every git commit. Tests are scoped to the file types you staged.

Pre-Commit Hook Behavior

Staged file typeChecks that run automatically
.ts / .tsx / .js / .jsxESLint (staged only), tsc --noEmit, npm test
.swiftSwiftLint (staged only), npm run test:ios
.kt / .ktscompileDebugKotlin (type check), lintDebug, npm run test:android

Requirements

  • SwiftLint: brew install swiftlint (skipped with a warning if not installed)
  • Android: Gradle wrapper in android/
If the hook fails, fix the issue and recommit. Never skip with --no-verify - this bypasses critical quality checks.

Pre-Commit Hook Source

The hook is defined in .husky/pre-commit:
.husky/pre-commit
#!/usr/bin/env sh

# Detect staged file types
STAGED_JS=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.(ts|tsx|js|jsx)$' || true)
STAGED_SWIFT=$(git diff --cached --name-only --diff-filter=ACMR | grep '\.swift$' | grep -v 'Pods/' | grep -v 'build/' || true)
STAGED_KOTLIN=$(git diff --cached --name-only --diff-filter=ACMR | grep -E '\.(kt|kts)$' || true)

# ── JS / TS ──
if [ -n "$STAGED_JS" ]; then
  echo "▶ JS/TS lint (staged files)..."
  npx lint-staged

  echo "▶ TypeScript type check..."
  npx tsc --noEmit

  echo "▶ JS/TS tests..."
  npm test
fi

# ── Swift / iOS ──
if [ -n "$STAGED_SWIFT" ]; then
  if command -v swiftlint >/dev/null 2>&1; then
    echo "▶ SwiftLint (staged files)..."
    echo "$STAGED_SWIFT" | tr '\n' '\0' | xargs -0 swiftlint lint --quiet
  else
    echo "⚠️  SwiftLint not installed — skipping Swift lint. Install: brew install swiftlint"
  fi

  echo "▶ iOS tests..."
  npm run test:ios
fi

# ── Kotlin / Android ──
if [ -n "$STAGED_KOTLIN" ]; then
  echo "▶ Kotlin type check (compileDebugKotlin)..."
  (cd android && ./gradlew compileDebugKotlin --quiet)

  echo "▶ Android lint..."
  (cd android && ./gradlew lintDebug --quiet)

  echo "▶ Android tests..."
  npm run test:android
fi

Writing Tests

Jest Tests (React Native)

Example test for a Zustand store:
src/stores/__tests__/chatStore.test.ts
import { renderHook, act } from '@testing-library/react-native';
import { useChatStore } from '../chatStore';

describe('chatStore', () => {
  beforeEach(() => {
    // Reset store before each test
    const { result } = renderHook(() => useChatStore());
    act(() => {
      result.current.clearAllConversations();
    });
  });

  it('creates a new conversation', () => {
    const { result } = renderHook(() => useChatStore());

    act(() => {
      result.current.createConversation('Test Chat');
    });

    expect(result.current.conversations).toHaveLength(1);
    expect(result.current.conversations[0].title).toBe('Test Chat');
  });
});

Android Tests (JUnit)

Example test for a Kotlin module:
android/app/src/test/java/ai/offgridmobile/PdfExtractorTest.kt
package ai.offgridmobile

import org.junit.Test
import org.junit.Assert.*

class PdfExtractorTest {
    @Test
    fun testPdfExtraction() {
        // Test PDF extraction logic
        val extractor = PdfExtractorModule()
        val result = extractor.extractText("test.pdf")
        assertNotNull(result)
    }
}

iOS Tests (XCTest)

Example test for a Swift module:
ios/OffgridMobileTests/PDFExtractorTests.swift
import XCTest
@testable import OffgridMobile

class PDFExtractorTests: XCTestCase {
    func testPDFExtraction() {
        let extractor = PDFExtractorModule()
        let result = extractor.extractText("test.pdf")
        XCTAssertNotNil(result)
    }
}

Troubleshooting

Use --forceExit to force Jest to exit after tests complete:
npx jest --forceExit
This is already included in the npm test script.
Ensure you’ve initialized the Android project:
cd android
./gradlew clean
cd ..
Check available simulators:
xcrun simctl list devices
Update the -destination flag in the test command to match an available simulator.
Install SwiftLint via Homebrew:
brew install swiftlint
Codecov will block the PR if coverage decreases. Add tests for new code:
  1. Check the Codecov report in the PR comment
  2. Identify uncovered lines
  3. Write tests to cover those lines
  4. Push updated code

Next Steps

Contributing Guide

Learn the full contribution workflow including PR reviews

Project Structure

Understand the codebase organization

Build docs developers (and LLMs) love