spaxel/dashboard/js/onboard.test.setup.js
jedarden 90e230f9d9 feat(dashboard): complete Phase 4 onboarding & OTA system
Interactive onboarding wizard:
- 8-step Web Serial-based provisioning flow
- Firmware flashing via esp-web-install-button (CDN)
- Live CSI waveform feedback during guided calibration
- Server-side provisioning with client-side fallback
- Serial JSON response handling with error mapping
- Post-calibration reinforcement card with link count

OTA firmware management:
- Firmware list with SHA-256 hashes and size display
- Per-node progress tracking (idle/pending/downloading/rebooting/verified/failed/rollback)
- Rolling update orchestration via REST API
- Status bar button with state indicators (normal/in-progress/has-update)
- Node list badges for OTA status and rollback warnings

Guided troubleshooting:
- First-time feature tooltips with 8s auto-dismiss
- Sequential tooltip tour triggered on first node connection
- Node offline cards with step-by-step recovery instructions
- Factory reset instructions modal
- Client-side link health check (60s no-frame threshold)
- Captive portal recovery documentation

Exit criteria: New ESP32-S3 from unboxed to streaming CSI in under 5 minutes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 21:21:55 -04:00

119 lines
3.5 KiB
JavaScript

/**
* Jest setup for onboard tests.
* Mocks Web Serial API, fetch, WebSocket, and sessionStorage.
*/
// Mock TextEncoderStream (not available in jsdom)
// Must provide functional readable/writable for pipeTo to work
var _lastEncodedData = '';
global.TextEncoderStream = class TextEncoderStream {
constructor() {
this.readable = {
pipeTo: jest.fn().mockResolvedValue(undefined),
};
this.writable = {
getWriter: jest.fn().mockReturnValue({
write: jest.fn(function (data) { _lastEncodedData = data; }),
close: jest.fn().mockResolvedValue(undefined),
releaseLock: jest.fn(),
}),
};
}
};
global.__getLastEncodedData = function () { return _lastEncodedData; };
global.__clearLastEncodedData = function () { _lastEncodedData = ''; };
// Mock TextDecoderStream (not available in jsdom)
var _lastDecodedChunk = '{"ok":true,"mac":"AA:BB:CC:DD:EE:FF"}\n';
global.TextDecoderStream = class TextDecoderStream {
constructor() {
this.readable = {
getReader: jest.fn().mockReturnValue({
read: jest.fn().mockResolvedValue({ done: false, value: _lastDecodedChunk }),
cancel: jest.fn().mockResolvedValue(undefined),
}),
};
this.writable = {
pipeTo: jest.fn().mockResolvedValue(undefined),
};
}
};
global.__setLastDecodedChunk = function (chunk) { _lastDecodedChunk = chunk; };
global.__getLastDecodedChunk = function () { return _lastDecodedChunk; };
// Mock ReadableStream/WritableStream (not available in jsdom)
global.ReadableStream = class ReadableStream {};
global.WritableStream = class WritableStream {};
// Mock navigator.serial
const mockPort = {
open: jest.fn().mockResolvedValue(undefined),
close: jest.fn().mockResolvedValue(undefined),
readable: {
pipeTo: jest.fn().mockResolvedValue(undefined),
},
writable: {
getWriter: jest.fn().mockReturnValue({
write: jest.fn().mockResolvedValue(undefined),
close: jest.fn().mockResolvedValue(undefined),
releaseLock: jest.fn(),
}),
},
};
Object.defineProperty(navigator, 'serial', {
value: {
requestPort: jest.fn().mockResolvedValue(mockPort),
getPorts: jest.fn().mockResolvedValue([mockPort]),
},
writable: true,
configurable: true,
});
// Mock fetch
global.fetch = jest.fn().mockResolvedValue({
ok: true,
json: jest.fn().mockResolvedValue([]),
});
// Mock WebSocket — use a factory so resetAllMocks doesn't break it
function _makeWSMock() {
return {
binaryType: 'arraybuffer',
close: jest.fn(),
send: jest.fn(),
readyState: 1,
onopen: null,
onclose: null,
onerror: null,
onmessage: null,
};
}
global.WebSocket = jest.fn().mockImplementation(function () {
return _makeWSMock();
});
// Mock crypto.randomUUID
Object.defineProperty(global, 'crypto', {
value: {
randomUUID: jest.fn().mockReturnValue('test-uuid-1234'),
},
});
// Mock customElements
global.customElements = {
get: jest.fn().mockReturnValue(null),
define: jest.fn(),
};
// Mock sessionStorage
var storage = {};
global.sessionStorage = {
getItem: jest.fn(function (key) { return storage[key] || null; }),
setItem: jest.fn(function (key, val) { storage[key] = val; }),
removeItem: jest.fn(function (key) { delete storage[key]; }),
clear: jest.fn(function () { storage = {}; }),
};
// Export mock port for tests
global.__mockPort = mockPort;