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>
119 lines
3.5 KiB
JavaScript
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;
|