Mock API server for Roku app testing. Define controlled HTTP responses, point your Roku app at the mock server, and test edge cases that are impossible to reproduce against production — empty feeds, error responses, slow backends, expired subscriptions, geo-blocked content.
You can't intercept a Roku device's HTTP traffic from outside. The app needs to know where to send requests. Most Roku dev builds support configurable API endpoints via registry values, launch params, or a dev-mode flag. This package handles the mock server side — getting the app to use it is your responsibility (via roku-odc registry injection, launch params, or app configuration).
npm install @danecodes/roku-mock
import { MockServer } from '@danecodes/roku-mock';
const mock = new MockServer({ port: 9090 });
// Static response
mock.get('/api/v1/feed', {
status: 200,
body: { items: [{ id: 'movie_1', title: 'Test Movie' }] },
});
// Dynamic handler with path params
mock.get('/api/v1/content/:id', (req) => {
return { status: 200, body: { id: req.params.id, title: 'Test Movie' } };
});
mock.post('/api/v1/analytics', { status: 204 });
await mock.start();
console.log(`Mock server at ${mock.baseUrl}`);
// ... run your tests ...
await mock.stop();The server binds to 0.0.0.0 by default so it's reachable from the Roku device on your local network. baseUrl auto-detects your machine's LAN IP.
Named response sets you can switch at runtime without restarting the server.
mock.scenario('happy-path', (m) => {
m.get('/api/v1/feed', { status: 200, body: { items: [...fullFeed] } });
m.get('/api/v1/user/profile', { status: 200, body: { name: 'Test', subscription: 'premium' } });
});
mock.scenario('empty-feed', (m) => {
m.get('/api/v1/feed', { status: 200, body: { items: [] } });
});
mock.scenario('expired-subscription', (m) => {
m.get('/api/v1/user/profile', { status: 200, body: { subscription: 'expired' } });
m.get('/api/v1/content/:id', { status: 403, body: { error: 'Subscription required' } });
});
mock.scenario('geo-blocked', (m) => {
m.get('/api/v1/content/:id', { status: 451, body: { error: 'Not available in your region' } });
});
mock.scenario('slow-backend', (m) => {
m.get('/api/v1/feed', { status: 200, body: { items: [...fullFeed] }, latency: 5000 });
});
// Activate one
mock.activate('happy-path');
await mock.start();
// Switch at runtime — no restart needed
mock.activate('empty-feed');
// Layer scenarios — later ones override earlier for conflicting routes
mock.activate('happy-path');
mock.activate('expired-subscription');
// feed comes from happy-path, content/:id returns 403Track what your Roku app actually calls.
// All recorded requests
const requests = mock.requests;
// [{ method: 'GET', path: '/api/v1/feed', headers: {...}, timestamp: Date }, ...]
// Wait for a specific request (test synchronization)
const req = await mock.waitForRequest('GET', '/api/v1/feed', { timeout: 5000 });
// Assert
expect(mock.requests.filter(r => r.path === '/api/v1/analytics')).toHaveLength(1);
// Clear
mock.clearRequests();// Global — applies to all routes
const mock = new MockServer({ port: 9090, latency: 200 });
// Per-route — overrides global
mock.get('/api/v1/feed', { status: 200, body: { items: [] }, latency: 5000 });Define scenarios in JSON instead of code.
{
"port": 9090,
"host": "0.0.0.0",
"latency": 0,
"scenarios": {
"happy-path": {
"routes": [
{ "method": "GET", "path": "/api/v1/feed", "status": 200, "bodyFile": "./fixtures/feed.json" },
{ "method": "GET", "path": "/api/v1/user/profile", "status": 200, "body": { "name": "Test" } }
]
},
"empty-feed": {
"routes": [
{ "method": "GET", "path": "/api/v1/feed", "status": 200, "body": { "items": [] } }
]
}
}
}Route options: body (inline JSON), bodyFile (path relative to config file), latency (ms), headers (custom response headers). Path params (:id) work in config routes too.
Point at a real backend, record all responses as JSON fixtures, then replay them offline.
import { Recorder } from '@danecodes/roku-mock';
const recorder = new Recorder({
target: 'https://api.example.com',
output: './fixtures/',
port: 9090,
});
await recorder.start();
// All requests are proxied to the real backend
// Responses saved as numbered JSON files in ./fixtures/
await recorder.stop();# Start with config file
roku-mock serve ./mock-config.json
roku-mock serve ./mock-config.json --scenario happy-path
# Inline route
roku-mock serve --get /api/feed --body '{"items":[]}'
# Options (override config file values)
roku-mock serve ./mock-config.json --port 8080 --host 0.0.0.0 --latency 200
# Record mode
roku-mock record --target https://api.example.com --output ./fixtures/Requires @danecodes/roku-ecp and/or @danecodes/roku-odc as optional peer dependencies.
import { MockServer, configureDevice, launchWithMock } from '@danecodes/roku-mock';
import { OdcClient } from '@danecodes/roku-odc';
import { EcpClient } from '@danecodes/roku-ecp';
const mock = new MockServer({ port: 9090 });
mock.activate('happy-path');
await mock.start();
// Set the app's API base URL via registry injection
const odc = new OdcClient('192.168.0.30');
await configureDevice(mock, odc, {
registrySection: 'config',
registryKey: 'apiBaseUrl',
});
// Or launch the app with the mock URL as a param
const ecp = new EcpClient('192.168.0.30');
await launchWithMock(mock, ecp, 'dev', { paramName: 'apiUrl' });
// Launches: /launch/dev?apiUrl=http://192.168.0.5:9090MIT