amaran-light-cli
Version:
Command line tool for controlling Aputure Amaran lights via WebSocket to a local Amaran desktop app.
279 lines • 13.3 kB
JavaScript
import { Command } from 'commander';
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import registerCommands from '../../commands.js';
import LightController from '../../deviceControl/lightControl.js';
import { MockLightServer } from '../../test/MockLightServer.js';
const TEST_PORT = 8090;
const WS_URL = `ws://localhost:${TEST_PORT}`;
describe('CLI Integration Tests', () => {
let server;
beforeAll(async () => {
server = new MockLightServer(TEST_PORT);
await new Promise((resolve) => setTimeout(resolve, 500));
});
afterAll(async () => {
await new Promise((resolve) => {
server.close(() => resolve());
});
await new Promise((resolve) => setTimeout(resolve, 100));
});
beforeEach(() => {
server.resetState();
});
const createDeps = () => {
const controller = new LightController(WS_URL, 'test-cli', undefined, false);
// Mock the disconnect to not actually close the socket immediately if needed,
// or arguably we DO want it to close to test the CLI behavior.
// The CLI calls disconnect() at the end.
// If we want to check state after, we should ensure the COMMAND has finished,
// and the server state should persist even if client disconnects.
return {
createController: async () => {
// Wait for connection and device list
await new Promise((resolve, reject) => {
const ws = controller.getWebSocket();
// If already open, just fetch list
const fetchDevices = () => {
controller.getDeviceList((success) => {
if (success)
resolve();
else
reject(new Error('Failed to fetch devices'));
});
};
if (ws.readyState === 1) {
fetchDevices();
}
else {
ws.once('open', () => {
// small delay to let onConnectionOpen trigger if needed, or just manual fetch
setTimeout(fetchDevices, 50);
});
ws.once('error', reject);
setTimeout(() => reject(new Error('Timeout connecting to mock server')), 2000);
}
});
return controller;
},
findDevice: (ctrl, deviceQuery) => {
const devices = ctrl.getDevices();
let device = devices.find((d) => d.node_id === deviceQuery || d.id === deviceQuery);
if (!device) {
const q = deviceQuery.toLowerCase();
device = devices.find((d) => {
const nm = (d.device_name || d.name || '').toLowerCase();
return nm.includes(q);
});
}
return device || null;
},
asyncCommand: (fn) => (...args) => fn(...args),
// We throw instead of process.exit for tests?
// Real asyncCommand does process.exit(1) on failure.
// We probably want to keep that or mock it.
// If we want to capture errors, we might mock process.exit.
loadConfig: () => ({}),
saveWsUrl: vi.fn(),
saveConfig: vi.fn(),
};
};
it('should turn on a light via power command', async () => {
const program = new Command();
program.exitOverride(); // Throw instead of exit
const deps = createDeps();
registerCommands(program, deps);
// Capture console output
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {
/* no-op */
});
await program.parseAsync(['node', 'test', 'on', '400J5-F2C008']);
// Wait for async command to complete (single device commands in CLI are not awaited)
await new Promise((resolve) => setTimeout(resolve, 100));
const state = server.getDeviceState('400J5-F2C008');
expect(state?.sleep).toBe(false); // On = sleep false
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('turned on'));
consoleSpy.mockRestore();
});
it('should turn off a light via power command', async () => {
const program = new Command();
program.exitOverride();
const deps = createDeps();
registerCommands(program, deps);
// Set initial state to ON (sleep false)
// Actually default is On (sleep false) in our mock server?
// Wait, MockLightServer: sleep: false by default.
// So "Off" should make sleep: true.
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {
/* no-op */
});
await program.parseAsync(['node', 'test', 'off', '400J5-F2C008']);
await new Promise((resolve) => setTimeout(resolve, 100));
const state = server.getDeviceState('400J5-F2C008');
expect(state?.sleep).toBe(true);
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('turned off'));
consoleSpy.mockRestore();
});
it('should set intensity via intensity command', async () => {
const program = new Command();
program.exitOverride();
const deps = createDeps();
registerCommands(program, deps);
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {
/* no-op */
});
await program.parseAsync(['node', 'test', 'intensity', '50', '400J5-F2C008']);
await new Promise((resolve) => setTimeout(resolve, 100));
const state = server.getDeviceState('400J5-F2C008');
// CLI takes 0-100, converts to 0-1000?
// Let's check src/commands/intensity.ts.
// Usually invalid args might fail, assuming 50 means 50% = 500
expect(state?.intensity).toBe(500);
consoleSpy.mockRestore();
});
it('should set CCT via cct command', async () => {
const program = new Command();
program.exitOverride();
const deps = createDeps();
registerCommands(program, deps);
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {
/* no-op */
});
await program.parseAsync(['node', 'test', 'cct', '5600', '400J5-F2C008']);
await new Promise((resolve) => setTimeout(resolve, 100));
const state = server.getDeviceState('400J5-F2C008');
expect(state?.cct).toBe(5600);
expect(state?.work_mode).toBe('CCT');
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('set to 5600K'));
consoleSpy.mockRestore();
});
it('should set CCT and intensity via cct command', async () => {
const program = new Command();
program.exitOverride();
const deps = createDeps();
registerCommands(program, deps);
await program.parseAsync(['node', 'test', 'cct', '3200', '-i', '80', '400J5-F2C008']);
await new Promise((resolve) => setTimeout(resolve, 100));
const state = server.getDeviceState('400J5-F2C008');
expect(state?.cct).toBe(3200);
expect(state?.intensity).toBe(800); // 80% = 800
expect(state?.work_mode).toBe('CCT');
});
it('should set HSI via hsi command', async () => {
const program = new Command();
program.exitOverride();
const deps = createDeps();
registerCommands(program, deps);
await program.parseAsync(['node', 'test', 'hsi', '240', '100', '50', '400J5-F2C008']);
await new Promise((resolve) => setTimeout(resolve, 100));
const state = server.getDeviceState('400J5-F2C008');
expect(state?.hue).toBe(240);
expect(state?.sat).toBe(100);
expect(state?.intensity).toBe(500); // 50% = 500
expect(state?.work_mode).toBe('HSI');
});
it('should send Set Color command via color command', async () => {
const program = new Command();
program.exitOverride();
const deps = createDeps();
registerCommands(program, deps);
// The CLI converts color name to something?
// Actually src/commands/color.ts simply calls controller.setColor
// which mock server just logs/acks but doesn't change state much unless we parse it.
// The MockLightServer currently doesn't update specific state for setColor,
// but the command logic should proceed and succeed.
// Check if color command throws
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {
/* no-op */
});
await program.parseAsync(['node', 'test', 'color', 'red', '400J5-F2C008']);
await new Promise((resolve) => setTimeout(resolve, 100));
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('color set to red'));
consoleSpy.mockRestore();
});
it('should list all devices via list command', async () => {
const program = new Command();
program.exitOverride();
const deps = createDeps();
registerCommands(program, deps);
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {
/* no-op */
});
await program.parseAsync(['node', 'test', 'list']);
// No async wait needed as list doesn't have an async device operation after the initial fetch which createController handles?
// Wait, list does createController -> getDevices (sync on client side) -> log -> disconnect.
// createController does async fetch.
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Test Light 1'));
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Test Light 2'));
consoleSpy.mockRestore();
});
it('should show status for a device via status command', async () => {
const program = new Command();
program.exitOverride();
const deps = createDeps();
registerCommands(program, deps);
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {
/* no-op */
});
// Ensure some known state
server.resetState();
// Default: sleep false (On), 0 intensity, 3200K
await program.parseAsync(['node', 'test', 'status', '400J5-F2C008']);
await new Promise((resolve) => setTimeout(resolve, 100)); // status fetches sleep status async
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Status for Test Light 1'));
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('State: On'));
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Intensity: 0%'));
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Temperature: 3200K')); // Default in mock
consoleSpy.mockRestore();
});
it('should run auto-cct command and update lights', async () => {
const program = new Command();
program.exitOverride();
const deps = createDeps();
registerCommands(program, deps);
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {
/* no-op */
});
// Ensure we have devices via reset
server.resetState();
// Run auto-cct with explicit location/time to avoid external factors
// Use Summer Solstice noon to ensure high CCT (Winter noon is lower due to solar altitude)
await program.parseAsync([
'node',
'test',
'auto-cct',
'--lat',
'40.7128',
'--lon',
'-74.0060',
'--time',
'2025-06-21T12:00:00-04:00',
]);
await new Promise((resolve) => setTimeout(resolve, 100));
// At noon it should be cool (6500K)
const state = server.getDeviceState('400J5-F2C008');
// Expect CCT to be changed.
expect(state?.cct).toBeGreaterThan(5000);
expect(state?.intensity).toBeGreaterThan(0);
consoleSpy.mockRestore();
});
it('should save and list config via config command', async () => {
const program = new Command();
program.exitOverride();
const deps = createDeps();
// Spy on saveConfig
const saveConfigSpy = vi.fn();
deps.saveConfig = saveConfigSpy;
registerCommands(program, deps);
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {
/* no-op */
});
// Set config
await program.parseAsync(['node', 'test', 'config', '--cct-min', '2500']);
expect(saveConfigSpy).toHaveBeenCalledWith(expect.objectContaining({ cctMin: 2500 }), expect.anything());
// Show config
await program.parseAsync(['node', 'test', 'config', '--show']);
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Current configuration:'));
consoleSpy.mockRestore();
});
});
//# sourceMappingURL=integration.test.js.map