Files
web-terminal/test/desktop/prefs.test.ts
Yaojia Wang 57725f7ef2 test: fix pre-existing failures — localStorage polyfill + prefs shape drift
- 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).
2026-07-07 21:09:49 +02:00

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 })
})
})