Testing
createTestApp()
createTestApp is the single entry point for integration tests. One call wires all 16 in-memory adapters, a MockApiClient, a real observable store, and a frozen config.
import { createTestApp } from '@objectifthunes/limestone-sdk/testing';
const app = createTestApp({ theme: myTheme });
Pass initialState to pre-seed auth or any other store slice:
const app = createTestApp({
theme: myTheme,
initialState: {
auth: {
status: 'authenticated',
user: { id: '1', name: 'Alice' },
tokens: { accessToken: 'tok', refreshToken: 'ref' },
},
},
});
TestApp fields
| Field | Type | Description |
|---|---|---|
store | Store<AppState> | Observable store, pre-seeded with initialState |
config | Readonly<LimestoneConfig> | Frozen config passed to the provider |
api | MockApiClient | Mock HTTP client with response registration and request recording |
biometrics | In-memory BiometricProvider with state | |
camera | In-memory CameraProvider with state | |
storage | In-memory SecureStorageProvider with state | |
haptics | In-memory HapticsProvider with state | |
notifications | In-memory NotificationProvider with state | |
purchases | In-memory PurchaseProvider with state | |
permissions | In-memory PermissionsProvider with state | |
share | In-memory ShareProvider with state | |
clipboard | In-memory ClipboardProvider with state | |
keychain | In-memory KeychainProvider with state | |
location | In-memory LocationProvider with state | |
mediaLibrary | In-memory MediaLibraryProvider with state | |
map | In-memory MapProvider with state | |
webBrowser | In-memory WebBrowserProvider with state | |
contacts | In-memory ContactsProvider with state | |
review | In-memory ReviewProvider with state |
MockApiClient
MockApiClient implements ApiClient and adds two extras: mockResponse and requests.
mockResponse
Register a canned response for a method + path combination.
app.api.mockResponse('GET', '/users/me', {
status: 200,
body: { id: '1', name: 'Alice' },
});
app.api.mockResponse('POST', '/sessions', {
status: 401,
body: { error: 'Invalid credentials' },
});
requests
An array of every request the client received, in order.
expect(app.api.requests).toHaveLength(1);
expect(app.api.requests[0]).toEqual({
method: 'GET',
path: '/users/me',
body: undefined,
headers: undefined,
});
Each entry is a RecordedRequest: { method, path, body?, headers? }.
Adapter state manipulation
Every in-memory adapter exposes a state object. Write to it directly to simulate hardware states and inject fixture data.
// Simulate biometrics unavailable
app.biometrics.state.available = false;
// Simulate a failed auth attempt
app.biometrics.state.nextResult = { success: false, error: 'locked' };
// Pre-seed secure storage
app.storage.state.store = { 'auth-token': 'abc123' };
// Simulate no network permissions
app.permissions.state.statuses = { location: 'denied', camera: 'granted' };
// Simulate a specific clipboard value
app.clipboard.state.value = 'copied text';
// Make review unavailable
app.review.state.available = false;
resetAll()
Call app.resetAll() between tests to return every adapter state, the store, and the request log to their initial values.
afterEach(() => {
app.resetAll();
});
Complete test example
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import { LimestoneProvider } from '@objectifthunes/limestone-sdk';
import { createTestApp } from '@objectifthunes/limestone-sdk/testing';
import { LoginForm } from '../src/screens/LoginForm';
import { myTheme } from '../src/theme';
const app = createTestApp({ theme: myTheme });
function renderWithApp(ui: React.ReactElement) {
return render(
<LimestoneProvider config={app.config}>
{ui}
</LimestoneProvider>,
);
}
afterEach(() => {
app.resetAll();
});
describe('LoginForm', () => {
it('submits valid credentials and calls the sessions endpoint', async () => {
app.api.mockResponse('POST', '/sessions', {
status: 200,
body: { accessToken: 'tok', refreshToken: 'ref', user: { id: '1' } },
});
const { getByLabelText, getByText } = renderWithApp(<LoginForm />);
fireEvent.changeText(getByLabelText('Email'), 'alice@example.com');
fireEvent.changeText(getByLabelText('Password'), 'secret1234');
fireEvent.press(getByText('Sign in'));
await waitFor(() => {
expect(app.api.requests).toHaveLength(1);
expect(app.api.requests[0].method).toBe('POST');
expect(app.api.requests[0].path).toBe('/sessions');
expect(app.api.requests[0].body).toEqual({
email: 'alice@example.com',
password: 'secret1234',
});
});
});
it('shows a field error when the server returns 401', async () => {
app.api.mockResponse('POST', '/sessions', {
status: 401,
body: { errors: { email: 'Invalid email or password' } },
});
const { getByLabelText, getByText, findByText } = renderWithApp(<LoginForm />);
fireEvent.changeText(getByLabelText('Email'), 'wrong@example.com');
fireEvent.changeText(getByLabelText('Password'), 'wrongpass');
fireEvent.press(getByText('Sign in'));
expect(await findByText('Invalid email or password')).toBeTruthy();
});
it('blocks submission when biometrics lock the session', async () => {
app.biometrics.state.available = true;
app.biometrics.state.nextResult = { success: false, error: 'locked' };
const bio = app.biometrics;
const result = await bio.authenticate({ promptMessage: 'Confirm' });
expect(result.success).toBe(false);
expect(result.error).toBe('locked');
});
});
Using individual in-memory doubles
Import the doubles directly when you do not need the full createTestApp setup.
import {
createInMemoryBiometrics,
createInMemorySecureStorage,
createInMemoryNotifications,
} from '@objectifthunes/limestone-sdk/testing';
const biometrics = createInMemoryBiometrics();
biometrics.state.available = true;
biometrics.state.nextResult = { success: true };
const result = await biometrics.authenticate();
expect(result.success).toBe(true);
const storage = createInMemorySecureStorage();
await storage.set('key', 'value');
expect(storage.state.store).toEqual({ key: 'value' });
All 16 in-memory doubles follow the same pattern: create, mutate state, call port methods, assert.