130 lines
5.3 KiB
JavaScript
130 lines
5.3 KiB
JavaScript
import { test } from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
import {
|
|
createInitialPedigreeState,
|
|
buildPedigreePayload,
|
|
serializeManualAncestor,
|
|
parseManualAncestor,
|
|
formatManualAncestorDisplay,
|
|
formatManualAncestorDetails,
|
|
pedigreeStateFromScan,
|
|
uncertainScanSlots,
|
|
} from './pedigree.js'
|
|
|
|
test('manual ancestor serialize/parse round-trips', () => {
|
|
const json = serializeManualAncestor({
|
|
name: 'Old Bob',
|
|
farm: 'Sunnyside',
|
|
ear_number: 'B-12',
|
|
weight_lbs: 4.5,
|
|
dob: 'Unknown',
|
|
})
|
|
const parsed = parseManualAncestor(json)
|
|
assert.equal(parsed.name, 'Old Bob')
|
|
assert.equal(parsed.farm, 'Sunnyside')
|
|
assert.equal(parsed.ear_number, 'B-12')
|
|
assert.equal(parsed.weight_lbs, 4.5)
|
|
assert.equal(parsed.dob, 'Unknown')
|
|
})
|
|
|
|
test('parseManualAncestor accepts legacy plain-text names', () => {
|
|
const parsed = parseManualAncestor('Just A Name')
|
|
assert.equal(parsed.name, 'Just A Name')
|
|
assert.equal(parsed.farm, '')
|
|
assert.equal(parsed.ear_number, '')
|
|
})
|
|
|
|
test('formatManualAncestorDisplay composes name, farm, ear number', () => {
|
|
const json = serializeManualAncestor({ name: 'Old Bob', farm: 'Sunnyside', ear_number: 'B-12' })
|
|
assert.equal(formatManualAncestorDisplay(json), 'Old Bob (Sunnyside) • Ear #B-12')
|
|
assert.equal(formatManualAncestorDisplay(serializeManualAncestor({ name: 'Solo' })), 'Solo')
|
|
})
|
|
|
|
test('buildPedigreePayload: empty tree -> all null', () => {
|
|
const payload = buildPedigreePayload(createInitialPedigreeState(), new Map())
|
|
assert.equal(payload.sire_id, null)
|
|
assert.equal(payload.dam_id, null)
|
|
assert.equal(payload.paternal_grandsire_name_text, null)
|
|
})
|
|
|
|
test('buildPedigreePayload: a linked parent writes the FK and locks (nulls) its ancestors', () => {
|
|
const state = createInitialPedigreeState()
|
|
state.sire = { mode: 'linked', id: 5, name: 'Jimmy' }
|
|
// Cascade cache holds Jimmy's own (empty) ancestry subtree.
|
|
const cache = new Map([[5, { id: 5, name: 'Jimmy', sire: null, dam: null }]])
|
|
|
|
const payload = buildPedigreePayload(state, cache)
|
|
assert.equal(payload.sire_id, 5)
|
|
assert.equal(payload.sire_name_text, null)
|
|
// Gen2 paternal slots are chain-derived from the linked sire, so they must
|
|
// stay null on the subject rather than being written as freeform text.
|
|
assert.equal(payload.paternal_grandsire_name_text, null)
|
|
assert.equal(payload.paternal_granddam_name_text, null)
|
|
})
|
|
|
|
test('buildPedigreePayload: a manual parent leaves deeper slots editable and persisted', () => {
|
|
const state = createInitialPedigreeState()
|
|
state.sire = { mode: 'manual', id: null, name: 'Bob' }
|
|
state.paternal_grandsire = { mode: 'manual', id: null, name: 'Old Bob' }
|
|
|
|
const payload = buildPedigreePayload(state, new Map())
|
|
assert.equal(payload.sire_id, null)
|
|
assert.equal(payload.sire_name_text, 'Bob')
|
|
assert.equal(payload.paternal_grandsire_name_text, 'Old Bob')
|
|
})
|
|
|
|
test('pedigreeStateFromScan links matched parents and makes named ancestors manual', () => {
|
|
const state = pedigreeStateFromScan({
|
|
ancestors: {
|
|
sire: { name: 'Hoppin-pop', farm: "Peachy Buns'", ear_number: 'HP1', dob: null, weight_lbs: null, uncertain: [] },
|
|
dam: { name: 'Black Betty', farm: "Cody's", ear_number: 'BB2', dob: '2024-03-04', weight_lbs: 4.25, uncertain: ['dob'] },
|
|
paternal_grandsire: { name: null, farm: null, ear_number: null, dob: null, weight_lbs: null, uncertain: [] },
|
|
},
|
|
matches: { sire: { id: 7, name: 'Hoppin-pop' }, dam: null },
|
|
})
|
|
assert.deepEqual(state.sire, { mode: 'linked', id: 7, name: 'Hoppin-pop' })
|
|
assert.equal(state.dam.mode, 'manual')
|
|
const dam = parseManualAncestor(state.dam.name)
|
|
assert.equal(dam.name, 'Black Betty')
|
|
assert.equal(dam.farm, "Cody's")
|
|
assert.equal(dam.dob, '2024-03-04')
|
|
assert.equal(dam.weight_lbs, 4.25)
|
|
// Unnamed or missing ancestors stay at the empty default.
|
|
assert.equal(state.paternal_grandsire.mode, 'unknown')
|
|
assert.equal(state.mm_granddam.mode, 'unknown')
|
|
})
|
|
|
|
test('uncertainScanSlots skips matched parents and certain ancestors', () => {
|
|
const slots = uncertainScanSlots({
|
|
ancestors: {
|
|
sire: { name: 'A', uncertain: ['ear_number'] },
|
|
dam: { name: 'B', uncertain: ['dob'] },
|
|
maternal_grandsire: { name: 'C', uncertain: [] },
|
|
},
|
|
matches: { sire: { id: 1, name: 'A' } },
|
|
})
|
|
assert.deepEqual([...slots], ['dam'])
|
|
})
|
|
|
|
test('manual ancestors default to 3 lbs and Unknown DOB, including legacy 0-weight entries', () => {
|
|
const fresh = parseManualAncestor(serializeManualAncestor({ name: 'No Info', farm: 'X', ear_number: 'A1' }))
|
|
assert.equal(fresh.weight_lbs, 3)
|
|
assert.equal(fresh.dob, 'Unknown')
|
|
const legacy = parseManualAncestor(JSON.stringify({ name: 'Old', farm: '', ear_number: '', weight_lbs: 0, dob: '' }))
|
|
assert.equal(legacy.weight_lbs, 3)
|
|
assert.equal(legacy.dob, 'Unknown')
|
|
assert.equal(parseManualAncestor('Plain Name').weight_lbs, 3)
|
|
const real = parseManualAncestor(serializeManualAncestor({ name: 'Real', weight_lbs: '4.5', dob: '2024-03-04' }))
|
|
assert.equal(real.weight_lbs, 4.5)
|
|
assert.equal(real.dob, '2024-03-04')
|
|
})
|
|
|
|
test('formatManualAncestorDetails shows weight and DOB', () => {
|
|
const fmt = (w) => `${w} lbs`
|
|
assert.equal(formatManualAncestorDetails(serializeManualAncestor({ name: 'A' }), fmt), '3 lbs · DOB Unknown')
|
|
assert.equal(
|
|
formatManualAncestorDetails(serializeManualAncestor({ name: 'B', weight_lbs: 5, dob: '2024-03-04' }), fmt),
|
|
'5 lbs · DOB 3/4/2024'
|
|
)
|
|
})
|