Testing¶
chatwire uses pytest for Python tests, vitest for the React frontend, and Jest for the mobile app.
Python tests (pytest)¶
Tests live in tests/. The conftest.py at the repo root adds the project directory to sys.path so top-level modules are importable without an editable install.
Run all tests¶
Run with verbose output¶
Run a specific test file¶
Run a specific test function¶
Run with coverage¶
Test configuration¶
pyproject.toml:
Writing Python tests¶
Tests follow the test_<module>.py naming convention. Use pytest.fixture for setup and tmp_path for temporary directories.
import pytest
from pathlib import Path
def test_config_roundtrip(tmp_path):
from config import save_config, load_config, CONFIG_PATH
# ... test body
Because the bridge and web process run as launchd agents, tests that require a running server should mock the relevant functions rather than starting real processes.
Mocking macOS-specific calls¶
Many bridge functions call osascript, sips, or tccutil. Tests mock these with monkeypatch or unittest.mock:
from unittest.mock import patch, MagicMock
def test_send_text(monkeypatch):
monkeypatch.setattr("chat_send.subprocess.run", lambda *a, **k: MagicMock(returncode=0))
from chat_send import send_text_confirm
result = send_text_confirm("+15551234567", "Hello")
assert result.status == "ok"
Frontend tests (vitest)¶
Frontend tests live in web/frontend/src/**/__tests__/ (colocated) or web/frontend/src/test/.
Run tests¶
Run with coverage¶
Writing frontend tests¶
Use @testing-library/react and @testing-library/user-event:
import { render, screen } from '@testing-library/react'
import { MessageBubble } from '../components/MessageBubble'
test('renders message text', () => {
render(
<MessageBubble
text="Hello!"
fromMe={false}
sender="Alice"
ts="2026-05-16T14:00:00Z"
/>
)
expect(screen.getByText('Hello!')).toBeInTheDocument()
})
Mock API calls with msw (Mock Service Worker):
import { setupServer } from 'msw/node'
import { http, HttpResponse } from 'msw'
const server = setupServer(
http.get('/api/v1/conversations', () => {
return HttpResponse.json([{ guid: 'iMessage;-;+1555', display_name: 'Alice' }])
})
)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
E2E tests (Playwright)¶
E2E tests live in web/frontend/e2e/. They require a running chatwire instance.
Run E2E tests¶
cd web/frontend
# Headless (CI)
npx playwright test
# With browser visible
npx playwright test --headed
# Specific test file
npx playwright test e2e/settings.spec.ts
# Debug mode (pauses on failure)
npx playwright test --debug
Writing E2E tests¶
import { test, expect } from '@playwright/test'
test('settings page loads', async ({ page }) => {
await page.goto('/app/')
await page.click('[data-testid="settings-button"]')
await expect(page.locator('h1')).toHaveText('Settings')
})
Accessibility tests¶
Playwright tests include axe accessibility checks:
import AxeBuilder from '@axe-core/playwright'
test('chat page has no accessibility violations', async ({ page }) => {
await page.goto('/app/')
const results = await new AxeBuilder({ page }).analyze()
expect(results.violations).toEqual([])
})
Mobile tests (Jest)¶
Mobile tests live in packages/mobile/src/__tests__/.
Run mobile tests¶
Writing mobile tests¶
import React from 'react'
import { render } from '@testing-library/react-native'
import { MessageBubble } from '../components/MessageBubble'
test('renders message text', () => {
const { getByText } = render(
<MessageBubble
text="Hello!"
fromMe={false}
sender="Alice"
/>
)
expect(getByText('Hello!')).toBeTruthy()
})
CI¶
Tests run on every pull request via GitHub Actions. The workflow:
- Python tests on macOS (latest)
- Frontend vitest
- Frontend typecheck
- Mobile Jest
E2E tests run separately as they require a running chatwire instance.
Note: GitHub Actions minutes are limited to 2000/month on the free tier. A self-hosted runner on plinux is planned for the near term — see the release process.
Test coverage targets¶
| Layer | Target |
|---|---|
| Python (bridge + CLI + config) | 70%+ |
| Python (web API) | 60%+ |
| Frontend components | 80%+ |
| Mobile screens | smoke test only |
Coverage is not enforced in CI — these are guidelines, not gates.