- jsdom 29's localStorage methods are undefined under this vitest config, so every frontend test calling localStorage.clear()/setItem() threw. Add a shared in-memory Web Storage polyfill via setupFiles (no-op when a working Storage exists / in node env). Fixes quick-reply, push, projects-panel (135 tests). - Update desktop prefs test: defaultPrefs gained remoteHosts/selectedHostId (remote-host mTLS work); sync EXPECTED_DEFAULTS and the round-trip fixture. Full suite now green: 1473 passed (was 142 failing).
223 lines
7.0 KiB
TypeScript
223 lines
7.0 KiB
TypeScript
/**
|
|
* test/desktop/prefs.test.ts — B3: unit tests for prefs defaults/validate/merge.
|
|
*
|
|
* Covers: defaultPrefs shape (and platform-independence); validatePrefs adopting
|
|
* valid fields and falling back per-field on wrong types / out-of-range port /
|
|
* empty shellPath / non-object raw / null / array; that it never throws; and
|
|
* mergePrefs immutability + ignoring undefined patch fields.
|
|
*/
|
|
|
|
import { describe, test, expect } from 'vitest'
|
|
import type { DesktopPrefs } from '../../desktop/src/types.js'
|
|
import { defaultPrefs, validatePrefs, mergePrefs } from '../../desktop/src/prefs.js'
|
|
|
|
const EXPECTED_DEFAULTS: DesktopPrefs = {
|
|
port: null,
|
|
lanSharing: false,
|
|
shellPath: null,
|
|
openAtLogin: false,
|
|
notifyOnApproval: true,
|
|
notifyOnStatusChange: true,
|
|
remoteHosts: [],
|
|
selectedHostId: null,
|
|
}
|
|
|
|
describe('defaultPrefs', () => {
|
|
test('returns the documented baseline shape (LAN sharing off by default)', () => {
|
|
// Arrange / Act
|
|
const prefs = defaultPrefs('darwin')
|
|
|
|
// Assert
|
|
expect(prefs).toEqual(EXPECTED_DEFAULTS)
|
|
})
|
|
|
|
test('is platform-independent (same defaults across platforms)', () => {
|
|
// Arrange / Act
|
|
const darwin = defaultPrefs('darwin')
|
|
const win32 = defaultPrefs('win32')
|
|
const linux = defaultPrefs('linux')
|
|
|
|
// Assert
|
|
expect(win32).toEqual(darwin)
|
|
expect(linux).toEqual(darwin)
|
|
})
|
|
|
|
test('returns a new object each call (no shared mutable state)', () => {
|
|
// Arrange / Act
|
|
const a = defaultPrefs('darwin')
|
|
const b = defaultPrefs('darwin')
|
|
|
|
// Assert
|
|
expect(a).not.toBe(b)
|
|
})
|
|
})
|
|
|
|
describe('validatePrefs', () => {
|
|
test('adopts every field from a fully-valid object', () => {
|
|
// Arrange
|
|
const raw = {
|
|
port: 8080,
|
|
lanSharing: true,
|
|
shellPath: '/bin/bash',
|
|
openAtLogin: true,
|
|
notifyOnApproval: false,
|
|
notifyOnStatusChange: false,
|
|
remoteHosts: [],
|
|
selectedHostId: null,
|
|
}
|
|
|
|
// Act
|
|
const prefs = validatePrefs(raw, 'darwin')
|
|
|
|
// Assert
|
|
expect(prefs).toEqual(raw)
|
|
})
|
|
|
|
test('falls back to defaults when raw is not an object', () => {
|
|
// Assert — strings, numbers, booleans are not prefs objects
|
|
expect(validatePrefs('nope', 'darwin')).toEqual(EXPECTED_DEFAULTS)
|
|
expect(validatePrefs(42, 'darwin')).toEqual(EXPECTED_DEFAULTS)
|
|
expect(validatePrefs(true, 'darwin')).toEqual(EXPECTED_DEFAULTS)
|
|
})
|
|
|
|
test('falls back to defaults when raw is null', () => {
|
|
expect(validatePrefs(null, 'darwin')).toEqual(EXPECTED_DEFAULTS)
|
|
})
|
|
|
|
test('falls back to defaults when raw is undefined', () => {
|
|
expect(validatePrefs(undefined, 'darwin')).toEqual(EXPECTED_DEFAULTS)
|
|
})
|
|
|
|
test('falls back to defaults when raw is an array (arrays are not records)', () => {
|
|
expect(validatePrefs([1, 2, 3], 'darwin')).toEqual(EXPECTED_DEFAULTS)
|
|
})
|
|
|
|
test('falls back per-field on wrong types, keeping valid siblings', () => {
|
|
// Arrange — lanSharing is a number (invalid); port/shellPath are valid
|
|
const raw = {
|
|
port: 3001,
|
|
lanSharing: 1,
|
|
shellPath: '/bin/zsh',
|
|
openAtLogin: 'yes',
|
|
notifyOnApproval: null,
|
|
notifyOnStatusChange: 0,
|
|
}
|
|
|
|
// Act
|
|
const prefs = validatePrefs(raw, 'darwin')
|
|
|
|
// Assert — valid fields adopted, invalid fields fall back to defaults
|
|
expect(prefs.port).toBe(3001)
|
|
expect(prefs.shellPath).toBe('/bin/zsh')
|
|
expect(prefs.lanSharing).toBe(false)
|
|
expect(prefs.openAtLogin).toBe(false)
|
|
expect(prefs.notifyOnApproval).toBe(true)
|
|
expect(prefs.notifyOnStatusChange).toBe(true)
|
|
})
|
|
|
|
test('rejects out-of-range and non-integer ports, keeping default null', () => {
|
|
expect(validatePrefs({ port: 0 }, 'darwin').port).toBeNull()
|
|
expect(validatePrefs({ port: 65536 }, 'darwin').port).toBeNull()
|
|
expect(validatePrefs({ port: -1 }, 'darwin').port).toBeNull()
|
|
expect(validatePrefs({ port: 3.5 }, 'darwin').port).toBeNull()
|
|
expect(validatePrefs({ port: NaN }, 'darwin').port).toBeNull()
|
|
expect(validatePrefs({ port: '3000' }, 'darwin').port).toBeNull()
|
|
})
|
|
|
|
test('accepts boundary ports (1 and 65535)', () => {
|
|
expect(validatePrefs({ port: 1 }, 'darwin').port).toBe(1)
|
|
expect(validatePrefs({ port: 65535 }, 'darwin').port).toBe(65535)
|
|
})
|
|
|
|
test('accepts an explicit null port', () => {
|
|
expect(validatePrefs({ port: null }, 'darwin').port).toBeNull()
|
|
})
|
|
|
|
test('rejects empty and whitespace-only shellPath, keeping default null', () => {
|
|
expect(validatePrefs({ shellPath: '' }, 'darwin').shellPath).toBeNull()
|
|
expect(validatePrefs({ shellPath: ' ' }, 'darwin').shellPath).toBeNull()
|
|
})
|
|
|
|
test('accepts an explicit null shellPath', () => {
|
|
expect(validatePrefs({ shellPath: null }, 'darwin').shellPath).toBeNull()
|
|
})
|
|
|
|
test('rejects a non-string shellPath, keeping default null', () => {
|
|
expect(validatePrefs({ shellPath: 123 }, 'darwin').shellPath).toBeNull()
|
|
})
|
|
|
|
test('never throws on hostile input shapes', () => {
|
|
// Assert — a grab-bag of malformed inputs must all return defaults, no throw
|
|
const hostile: unknown[] = [Symbol('x'), () => 0, new Map(), 0n]
|
|
for (const raw of hostile) {
|
|
expect(() => validatePrefs(raw, 'darwin')).not.toThrow()
|
|
expect(validatePrefs(raw, 'darwin')).toEqual(EXPECTED_DEFAULTS)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('mergePrefs', () => {
|
|
test('applies defined patch fields over the base', () => {
|
|
// Arrange
|
|
const base = defaultPrefs('darwin')
|
|
|
|
// Act
|
|
const merged = mergePrefs(base, { port: 4000, lanSharing: true })
|
|
|
|
// Assert
|
|
expect(merged.port).toBe(4000)
|
|
expect(merged.lanSharing).toBe(true)
|
|
expect(merged.shellPath).toBeNull()
|
|
})
|
|
|
|
test('ignores undefined patch fields (does not clobber base)', () => {
|
|
// Arrange
|
|
const base = mergePrefs(defaultPrefs('darwin'), { port: 5000, shellPath: '/bin/bash' })
|
|
|
|
// Act — an explicit-undefined patch field must be ignored
|
|
const merged = mergePrefs(base, { port: undefined, lanSharing: true })
|
|
|
|
// Assert
|
|
expect(merged.port).toBe(5000)
|
|
expect(merged.shellPath).toBe('/bin/bash')
|
|
expect(merged.lanSharing).toBe(true)
|
|
})
|
|
|
|
test('applies falsy-but-defined patch fields (false / null)', () => {
|
|
// Arrange
|
|
const base = mergePrefs(defaultPrefs('darwin'), { port: 6000, notifyOnApproval: true })
|
|
|
|
// Act
|
|
const merged = mergePrefs(base, { port: null, notifyOnApproval: false })
|
|
|
|
// Assert — null/false are defined values and must overwrite
|
|
expect(merged.port).toBeNull()
|
|
expect(merged.notifyOnApproval).toBe(false)
|
|
})
|
|
|
|
test('does not mutate the base object (immutability)', () => {
|
|
// Arrange
|
|
const base = defaultPrefs('darwin')
|
|
const snapshot = { ...base }
|
|
|
|
// Act
|
|
const merged = mergePrefs(base, { port: 7000 })
|
|
|
|
// Assert — base untouched, a new object returned
|
|
expect(base).toEqual(snapshot)
|
|
expect(merged).not.toBe(base)
|
|
})
|
|
|
|
test('does not mutate the patch object', () => {
|
|
// Arrange
|
|
const base = defaultPrefs('darwin')
|
|
const patch = { port: 8000 }
|
|
|
|
// Act
|
|
mergePrefs(base, patch)
|
|
|
|
// Assert
|
|
expect(patch).toEqual({ port: 8000 })
|
|
})
|
|
})
|