Testing Guide

Quick Commands

# Run all tests
./scripts/test-all.sh

# Platform-specific
cd mac && xcodebuild test
cd ios && ./scripts/test-with-coverage.sh
cd web && pnpm test

# With coverage
cd web && pnpm test:coverage

Test Structure

tests/
├── unit/           # Unit tests
├── integration/    # Integration tests
├── e2e/           # End-to-end tests
└── fixtures/      # Test data

Unit Testing

Swift (XCTest)

// mac/VibeTunnelTests/ServerManagerTests.swift
import XCTest
@testable import VibeTunnel

class ServerManagerTests: XCTestCase {
    func testServerStart() async throws {
        let manager = ServerManager()
        
        try await manager.start()
        
        XCTAssertTrue(manager.isRunning)
        XCTAssertEqual(manager.port, "4020")
    }
    
    func testPortValidation() {
        XCTAssertThrowsError(try validatePort("abc"))
        XCTAssertNoThrow(try validatePort("8080"))
    }
}

TypeScript (Vitest)

// web/tests/session-manager.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { SessionManager } from '../src/server/services/session-manager';

describe('SessionManager', () => {
  let manager: SessionManager;
  
  beforeEach(() => {
    manager = new SessionManager();
  });
  
  it('creates session', async () => {
    const session = await manager.create({
      shell: '/bin/bash',
      cols: 80,
      rows: 24
    });
    
    expect(session.id).toBeDefined();
    expect(session.status).toBe('running');
  });
});

Integration Testing

API Testing

// web/tests/integration/api.test.ts
import request from 'supertest';
import { app } from '../../src/server/app';

describe('API Integration', () => {
  it('creates session via API', async () => {
    const response = await request(app)
      .post('/api/sessions')
      .send({ shell: '/bin/bash' })
      .expect(201);
    
    expect(response.body).toHaveProperty('id');
    expect(response.body.status).toBe('running');
  });
});

WebSocket Testing

// web/tests/integration/websocket.test.ts
import { WebSocket } from 'ws';

describe('WebSocket Integration', () => {
  it('connects to session', async () => {
    const ws = new WebSocket('ws://localhost:4020/api/sessions/test/ws');
    
    await new Promise((resolve) => {
      ws.on('open', resolve);
    });
    
    ws.send(JSON.stringify({ type: 'input', data: 'echo test\n' }));
    
    const message = await new Promise((resolve) => {
      ws.on('message', resolve);
    });
    
    expect(message.toString()).toContain('test');
  });
});

E2E Testing

Playwright Setup

// web/playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests/e2e',
  use: {
    baseURL: 'http://localhost:4020',
    trace: 'on-first-retry',
  },
  webServer: {
    command: 'pnpm dev',
    port: 4020,
    reuseExistingServer: !process.env.CI,
  },
});

E2E Tests

// web/tests/e2e/terminal.test.ts
import { test, expect } from '@playwright/test';

test('complete terminal workflow', async ({ page }) => {
  // Navigate to app
  await page.goto('/');
  
  // Create new terminal
  await page.click('button:has-text("New Terminal")');
  
  // Wait for terminal to load
  const terminal = page.locator('.terminal');
  await expect(terminal).toBeVisible();
  
  // Type command
  await page.keyboard.type('echo "Hello, VibeTunnel"');
  await page.keyboard.press('Enter');
  
  // Verify output
  await expect(terminal).toContainText('Hello, VibeTunnel');
  
  // Close session
  await page.click('button[aria-label="Close terminal"]');
  await expect(terminal).not.toBeVisible();
});

Performance Testing

Load Testing

// tests/performance/load.js
import { check } from 'k6';
import ws from 'k6/ws';

export default function() {
  const url = 'ws://localhost:4020/api/sessions/test/ws';
  
  ws.connect(url, {}, function(socket) {
    socket.on('open', () => {
      socket.send(JSON.stringify({ type: 'input', data: 'ls\n' }));
    });
    
    socket.on('message', (data) => {
      check(data, {
        'received response': (d) => d.length > 0,
      });
    });
  });
}

export const options = {
  vus: 100,        // 100 virtual users
  duration: '30s', // 30 second test
};

Benchmark Suite

// tests/performance/benchmark.ts
import { bench, describe } from 'vitest';

describe('Buffer encoding performance', () => {
  bench('encode 1KB', () => {
    encodeBuffer('x'.repeat(1024));
  });
  
  bench('encode 10KB', () => {
    encodeBuffer('x'.repeat(10240));
  });
});

Test Coverage

Coverage Requirements

ComponentTargetCurrent
Server80%85%
Client70%72%
Mac App60%65%
iOS App75%78%

Generate Reports

# Web coverage
cd web && pnpm test:coverage

# iOS coverage
cd ios && ./scripts/test-with-coverage.sh

# View HTML report
open coverage/index.html

Testing External Devices

iPad/iPhone Testing

# 1. Start dev server on all interfaces
cd web && pnpm dev --host 0.0.0.0

# 2. Get Mac IP
ifconfig | grep inet

# 3. Access from device
# http://192.168.1.100:4021

Cross-Browser Testing

// playwright.config.ts
projects: [
  { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
  { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
  { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  { name: 'Mobile Safari', use: { ...devices['iPhone 13'] } },
]

Mocking & Fixtures

Mock PTY

// tests/mocks/pty.ts
export class MockPTY {
  write(data: string) {
    this.emit('data', `mock: ${data}`);
  }
  
  resize(cols: number, rows: number) {
    this.cols = cols;
    this.rows = rows;
  }
}

Test Fixtures

// tests/fixtures/sessions.ts
export const mockSession = {
  id: 'test-session-123',
  name: 'Test Session',
  status: 'running',
  created: new Date(),
  pid: 12345,
};

CI/CD Testing

GitHub Actions

# .github/workflows/test.yml
name: Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: 20
          
      - name: Install dependencies
        run: |
          cd web && pnpm install
          
      - name: Run tests
        run: ./scripts/test-all.sh
        
      - name: Upload coverage
        uses: codecov/codecov-action@v3

Debugging Tests

Debug Swift Tests

# Run with verbose output
xcodebuild test -verbose

# Debug specific test
xcodebuild test -only-testing:VibeTunnelTests/ServerManagerTests/testServerStart

Debug TypeScript Tests

# Run with inspector
node --inspect-brk ./node_modules/.bin/vitest

# Run single test file
pnpm test session-manager.test.ts

# Watch mode
pnpm test --watch

Best Practices

  1. Test naming: Use descriptive names like shouldCreateSessionWithCustomShell
  2. Isolation: Each test should be independent
  3. Cleanup: Always cleanup resources (sessions, files, connections)
  4. Assertions: Test both success and error cases
  5. Speed: Keep unit tests under 100ms each
  6. Flakiness: Retry flaky tests, investigate root cause

Common Issues

IssueSolution
Tests timeoutIncrease timeout, check async
Port conflictsUse random ports in tests
Flaky WebSocketAdd connection retry logic
Coverage gapsAdd tests for error paths

See Also