Files
GerbilManager/gerbil-manager-web/src/genetics/__tests__/genetics.test.ts
Gulum efce79b3fa GEN-3g: CP colourpoint het-naming rule — '-Hell' == cchm/ch (breeder C9 answer)
- colourpointName(): A- cchm/ch now yields 'CP-<base>-Hell' (was 'CP-<base>');
  A- cchm/cchm unchanged. Non-agouti branch (Marder/Siam/Zobel/Zobel-Hell)
  unchanged — already used the het token.
- Added 5 new catalog entries: CP-Agouti-Hell, CP-Silberagouti-Hell,
  CP-Algierfuchs-Hell, CP-Polarfuchs-Hell, CP-Orangeschimmel-Hell.
  CP-Fuchs-Hell was already present. Catalog 61 -> 66.
- Regenerated colorVarietySeed.generated.json (66 rows for Pam DATA-2 re-seed).
- Tests: 82 -> 82 (reworded het case) + 1 new GEN-3g describe block. All pass.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-06-06 15:14:23 +02:00

526 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest'
import {
add,
divide,
frac,
multiply,
toNumber,
toString,
equals,
} from '../fraction'
import {
fromDisplayString,
makeGenotype,
toDisplayString,
toJSON,
fromJSON,
wildType,
extractGenotypeFlags,
} from '../genotype'
import { combineLocus } from '../punnett'
import { LOCI, type LocusKey } from '../loci'
import {
farbschlagFor,
genotypeToFarbschlag,
representativeGenotype,
BASE_COLORS,
CATALOG,
CATALOG_SIZE,
} from '../catalog'
/** The first 18 entries are the frozen contract names (must round-trip exactly). */
const FROZEN_COUNT = 17
import { breed } from '../breed'
import { GeneticsWarningCode } from '../warnings'
describe('Fraction', () => {
it('reduces and does exact arithmetic', () => {
expect(toString(frac(2, 4))).toBe('1/2')
expect(toString(add(frac(1, 4), frac(1, 4)))).toBe('1/2')
expect(toString(multiply(frac(1, 2), frac(1, 2)))).toBe('1/4')
expect(toString(divide(frac(1, 4), frac(3, 4)))).toBe('1/3')
expect(equals(frac(3, 6), frac(1, 2))).toBe(true)
})
})
describe('Genotype serialization', () => {
it('round-trips display string <-> structured form', () => {
const g = fromDisplayString('Aa CC Dd EE GG Pp Spsp rere')
expect(toDisplayString(g)).toBe('Aa CC Dd EE GG Pp Spsp rere')
expect(toDisplayString(fromJSON(toJSON(g)))).toBe('Aa CC Dd EE GG Pp Spsp rere')
})
it('parses multi-char C-series alleles via maximal munch', () => {
const g = fromDisplayString('aa cchmch DD EE GG PP spsp rere')
expect(g.C).toEqual(['cchm', 'ch'])
})
it('orders allele pairs most-dominant-first', () => {
const g = makeGenotype({
A: ['a', 'A'],
C: ['ch', 'C'],
D: ['D', 'D'],
E: ['E', 'E'],
G: ['G', 'G'],
P: ['p', 'P'],
Sp: ['sp', 'sp'],
Re: ['re', 're'],
Sls: ['sl', 'sl'],
})
expect(g.A).toEqual(['A', 'a'])
expect(g.C).toEqual(['C', 'ch'])
expect(g.P).toEqual(['P', 'p'])
})
it('wild type is AA CC DD EE GG PP spsp rere', () => {
expect(toDisplayString(wildType())).toBe('AA CC DD EE GG PP spsp rere')
})
})
describe('Punnett single locus', () => {
it('Aa x Aa -> 1/4 AA, 1/2 Aa, 1/4 aa', () => {
const dist = combineLocus('A', ['A', 'a'], ['A', 'a'])
expect(toString(dist.get('AA')!.probability)).toBe('1/4')
expect(toString(dist.get('Aa')!.probability)).toBe('1/2')
expect(toString(dist.get('aa')!.probability)).toBe('1/4')
})
it('AA x aa -> all Aa', () => {
const dist = combineLocus('A', ['A', 'A'], ['a', 'a'])
expect(dist.size).toBe(1)
expect(toString(dist.get('Aa')!.probability)).toBe('1')
})
})
describe('Worked example from research report', () => {
// Agouti AA CC DD EE GG PP x Platin aa CC DD EE GG pp -> all Aa Cc Dd Ee Gg Pp
// (every locus where parents differ becomes heterozygous with prob 1)
it('Agouti x Platin yields a single uniform heterozygous genotype', () => {
const father = fromDisplayString('AA CC DD EE GG PP spsp rere')
const mother = fromDisplayString('aa CC DD EE GG pp spsp rere')
const result = breed(father, mother)
expect(result.offspring).toHaveLength(1)
const only = result.offspring[0]
expect(only.genotype).toBe('Aa CC DD EE GG Pp spsp rere')
expect(only.probability.text).toBe('1')
expect(result.warnings).toHaveLength(0)
})
})
describe('Schecke × Schecke lethality (SpSp)', () => {
it('removes SpSp, renormalises, and warns ~25% fewer young', () => {
// Spsp x Spsp at the Sp locus -> 1/4 SpSp (lethal), 1/2 Spsp, 1/4 spsp.
// After removing SpSp the survivors renormalise to 2/3 Spsp : 1/3 spsp.
const father = fromDisplayString('AA CC DD EE GG PP Spsp rere')
const mother = fromDisplayString('AA CC DD EE GG PP Spsp rere')
const result = breed(father, mother)
// No surviving genotype is SpSp.
expect(result.offspring.every((o) => !o.genotype.includes('SpSp'))).toBe(true)
// Probabilities renormalise to sum to exactly 1.
const sum = result.offspring.reduce((acc, o) => acc + o.probability.value, 0)
expect(sum).toBeCloseTo(1, 10)
// Survivors: 2/3 checkered (Spsp), 1/3 non-checkered (spsp).
const spsp = result.offspring.find((o) => o.genotype.includes('Spsp'))!
const homRec = result.offspring.find((o) => o.genotype.includes('spsp'))!
expect(spsp.probability.text).toBe('2/3')
expect(homRec.probability.text).toBe('1/3')
// Warning emitted, carrying the 1/4 young-lost stat.
const warn = result.warnings.find((w) => w.code === GeneticsWarningCode.ScheckeLethal)
expect(warn).toBeDefined()
expect(warn!.detail?.youngLostFraction).toBe('1/4')
})
})
describe('Rex × Rex semi-lethality (ReRe)', () => {
it('keeps ReRe but emits a semi-lethal warning', () => {
const father = fromDisplayString('AA CC DD EE GG PP spsp Rere')
const mother = fromDisplayString('AA CC DD EE GG PP spsp Rere')
const result = breed(father, mother)
// ReRe survives (semi-lethal, not removed).
expect(result.offspring.some((o) => o.genotype.includes('ReRe'))).toBe(true)
const sum = result.offspring.reduce((acc, o) => acc + o.probability.value, 0)
expect(sum).toBeCloseTo(1, 10)
const warn = result.warnings.find((w) => w.code === GeneticsWarningCode.RexSemiLethal)
expect(warn).toBeDefined()
})
})
describe('Farbschlag catalog', () => {
it('names the wild type Agouti', () => {
expect(farbschlagFor(wildType()).name).toBe('Agouti')
})
it('names Platin (aa CC DD EE GG pp)', () => {
expect(farbschlagFor(fromDisplayString('aa CC DD EE GG pp spsp rere')).name).toBe('Platin')
})
it('names Schwarz (aa CC DD EE GG PP)', () => {
expect(farbschlagFor(fromDisplayString('aa CC DD EE GG PP spsp rere')).name).toBe('Schwarz')
})
it('appends Schecke and Rex modifiers', () => {
expect(farbschlagFor(fromDisplayString('AA CC DD EE GG PP Spsp rere')).name).toBe('Agouti Schecke')
expect(farbschlagFor(fromDisplayString('AA CC DD EE GG PP Spsp Rere')).name).toBe(
'Agouti Schecke Rex',
)
})
it('falls back to Unbekannter Farbschlag for uncatalogued genotypes', () => {
// E-dominant (no Fuchs/Schimmel family) + an uncatalogued cchm/dd/gg/pp combo.
const match = farbschlagFor(fromDisplayString('aa CC dd EE gg pp spsp rere'))
expect(match.unknown).toBe(true)
expect(match.name).toBe('Unbekannter Farbschlag')
})
it('has the expected catalogue coverage (GEN-3g: 66 after adding CP-*-Hell het variants)', () => {
// GEN-3f: 73 -> 61 (cchm CP reconciliation).
// GEN-3g: +5 het variants (CP-Agouti/Silberagouti/Algierfuchs/Polarfuchs/Orangeschimmel -Hell),
// giving 61 + 5 = 66. CP-Fuchs-Hell was already counted.
expect(CATALOG_SIZE).toBe(66)
})
it('frozen contract names round-trip to themselves (DB-key guard)', () => {
// The first 18 are the frozen ColorVariety keys — their representative
// genotype MUST resolve back to their own name, never a later variety.
for (const entry of BASE_COLORS.slice(0, FROZEN_COUNT)) {
expect(genotypeToFarbschlag(representativeGenotype(entry))).toBe(entry.name)
}
})
it('every entry resolves to a named variety (no self-Unbekannt)', () => {
// Each catalogued variety's representative genotype must resolve to SOME
// named variety — never "Unbekannt". Genotype-identical synonyms (e.g.
// "-hell") or entries that omit a locus may resolve to an earlier primary;
// that's expected first-match behaviour, not an error.
for (const entry of BASE_COLORS) {
expect(farbschlagFor(representativeGenotype(entry)).unknown).toBe(false)
}
})
it('has no duplicate variety names and only valid allele tokens', () => {
const names = BASE_COLORS.map((e) => e.name)
expect(new Set(names).size).toBe(names.length)
for (const entry of BASE_COLORS) {
for (const [locus, value] of Object.entries(entry.tokens)) {
// GEN-3f: a token may be a heterozygous pair "x/y" (e.g. C: 'cchm/ch'
// for the het colourpoints Siam/Zobel-Hell) — validate each allele.
for (const allele of value.split('/')) {
expect(LOCI[locus as LocusKey].alleles).toContain(allele)
}
}
}
})
it('CATALOG seed view mirrors the ColorVariety table shape', () => {
expect(CATALOG).toHaveLength(CATALOG_SIZE)
expect(CATALOG[0]).toMatchObject({ name: 'Pink Eyed White (PEW)', sortOrder: 0 })
// Every row has a non-empty canonical genotype display string and unique name.
expect(new Set(CATALOG.map((c) => c.name)).size).toBe(CATALOG.length)
expect(CATALOG.every((c) => /^[A-Za-z?]+( [A-Za-z?]+){7}$/.test(c.canonicalGenotype))).toBe(true)
})
it('genotypeToFarbschlag (DATA-1 denormalization contract) returns the plain name', () => {
expect(genotypeToFarbschlag(wildType())).toBe('Agouti')
expect(genotypeToFarbschlag(fromDisplayString('aa CC DD EE GG pp spsp rere'))).toBe('Platin')
expect(genotypeToFarbschlag(fromDisplayString('aa CC dd EE gg pp spsp rere'))).toBe(
'Unbekannter Farbschlag',
)
})
})
describe('Partially-unknown parents (wildcards)', () => {
it('handles an A? parent (phenotype agouti, genotype unknown)', () => {
// A? x aa at the A locus -> father gamete: 1/2 A, 1/4 (each of A,a) from "?"
// = effectively 3/4 A, 1/4 a ; mother always a.
// Offspring: 3/4 Aa, 1/4 aa.
const father = makeGenotype({
A: ['A', '?'],
C: ['C', 'C'],
D: ['D', 'D'],
E: ['E', 'E'],
G: ['G', 'G'],
P: ['P', 'P'],
Sp: ['sp', 'sp'],
Re: ['re', 're'],
Sls: ['sl', 'sl'],
})
const mother = fromDisplayString('aa CC DD EE GG PP spsp rere')
const result = breed(father, mother)
const agouti = result.byFarbschlag.find((f) => f.farbschlag === 'Agouti')!
const schwarz = result.byFarbschlag.find((f) => f.farbschlag === 'Schwarz')!
expect(agouti.probability.text).toBe('3/4')
expect(schwarz.probability.text).toBe('1/4')
})
})
describe('Probabilities are exact fractions summing to 1', () => {
it('Aa Cc x Aa Cc -> 9 genotypes summing to 1', () => {
const father = fromDisplayString('Aa Cch DD EE GG PP spsp rere')
const mother = fromDisplayString('Aa Cch DD EE GG PP spsp rere')
const result = breed(father, mother)
const sum = result.offspring.reduce(
(acc, o) => add(acc, o.probability.fraction),
frac(0, 1),
)
expect(toNumber(sum)).toBeCloseTo(1, 10)
})
})
describe('GEN-3a: Uw=G alias', () => {
it('Uwuw parses as Gg, UwUw as GG', () => {
expect(fromDisplayString('AA CC DD EE Uwuw PP spsp rere').G).toEqual(['G', 'g'])
expect(fromDisplayString('AA CC DD EE UwUw PP spsp rere').G).toEqual(['G', 'G'])
expect(fromDisplayString('AA CC DD EE uwuw PP spsp rere').G).toEqual(['g', 'g'])
})
it('always RENDERS G, never Uw (breeder preference)', () => {
// Uw/uw is an input/import alias only; output must echo G/g.
expect(toDisplayString(fromDisplayString('AA CC DD EE Uwuw PP spsp rere'))).toBe(
'AA CC DD EE Gg PP spsp rere',
)
expect(toDisplayString(fromDisplayString('AA CC DD EE uwuw PP spsp rere'))).not.toContain('uw')
})
})
describe('GEN-3a: second spotting locus Sls (WP)', () => {
it('S(l)s(l) and WP both parse to the Sls heterozygote Slsl', () => {
expect(fromDisplayString('AA CC DD EE GG PP spsp rere S(l)s(l)').Sls).toEqual(['Sl', 'sl'])
expect(fromDisplayString('AA CC DD EE GG PP spsp rere WP').Sls).toEqual(['Sl', 'sl'])
})
it('toDisplayString omits wild-type Sls but shows Slsl', () => {
expect(toDisplayString(wildType())).toBe('AA CC DD EE GG PP spsp rere')
expect(toDisplayString(fromDisplayString('AA CC DD EE GG PP spsp rere WP'))).toBe(
'AA CC DD EE GG PP spsp rere Slsl',
)
})
it('WP × WP: S(l)S(l) is lethal — removed, renormalized, SLS_LETHAL warning', () => {
const parent = fromDisplayString('AA CC DD EE GG PP spsp rere Slsl')
const result = breed(parent, parent)
expect(result.offspring.every((o) => !o.genotype.includes('SlSl'))).toBe(true)
const sum = result.offspring.reduce((acc, o) => acc + o.probability.value, 0)
expect(sum).toBeCloseTo(1, 10)
// survivors 2/3 Slsl : 1/3 slsl
const wp = result.offspring.find((o) => o.genotype.includes('Slsl'))!
expect(wp.probability.text).toBe('2/3')
const warn = result.warnings.find((w) => w.code === GeneticsWarningCode.SlsLethal)
expect(warn?.detail?.youngLostFraction).toBe('1/4')
})
it('Sp × Sls → Superschecke deafness warning', () => {
const father = fromDisplayString('AA CC DD EE GG PP Spsp rere')
const mother = fromDisplayString('AA CC DD EE GG PP spsp rere Slsl')
const result = breed(father, mother)
expect(
result.warnings.some((w) => w.code === GeneticsWarningCode.SuperscheckeDeaf),
).toBe(true)
})
})
describe('GEN-3a: flag/metadata tokens tolerated', () => {
it('dea/taub/Dea/DP/WFNZ/RV/GV do not break parsing (stripped)', () => {
const g = fromDisplayString('AA CC DD EE GG PP spsp rere dea WFNZ DP RV GV')
expect(toDisplayString(g)).toBe('AA CC DD EE GG PP spsp rere')
})
it('extractGenotypeFlags reads deafness + tags', () => {
expect(extractGenotypeFlags('AA CC DD EE GG PP spsp rere dea WFNZ').deaf).toBe(true)
expect(extractGenotypeFlags('AA CC DD EE GG PP spsp rere Dea').deaf).toBe(false)
expect(extractGenotypeFlags('AA CC DD EE GG PP spsp rere WFNZ RV').tags).toEqual(['WFNZ', 'RV'])
})
})
describe('GEN-3a: Schimmel catalog fix (breeder C5)', () => {
it('efef base -> Orangeschimmel (not Schwarzschimmel)', () => {
expect(genotypeToFarbschlag(fromDisplayString('AA CC DD efef GG PP spsp rere'))).toBe(
'Orangeschimmel',
)
})
it('efef pp -> Rotaugenschimmel, efef gg -> Silberschimmel', () => {
expect(genotypeToFarbschlag(fromDisplayString('AA CC DD efef GG pp spsp rere'))).toBe(
'Rotaugenschimmel',
)
expect(genotypeToFarbschlag(fromDisplayString('AA CC DD efef gg PP spsp rere'))).toBe(
'Silberschimmel',
)
})
it('no Schwarzschimmel anywhere in the catalog', () => {
expect(BASE_COLORS.some((e) => e.name === 'Schwarzschimmel')).toBe(false)
})
})
describe("GEN-3c: unknown allele displays as '-' (stored as '?')", () => {
it("accepts '-' input, stores '?', displays '-'", () => {
const g = fromDisplayString('Aa C- DD EE GG Pp spsp rere')
expect(g.C).toEqual(['C', '?']) // stored internal contract stays '?'
expect(toDisplayString(g)).toBe('Aa C- DD EE GG Pp spsp rere') // displayed as '-'
})
it("'?' and '-' inputs are equivalent", () => {
expect(toDisplayString(fromDisplayString('Aa C? DD EE GG Pp spsp rere'))).toBe(
'Aa C- DD EE GG Pp spsp rere',
)
})
})
describe('GEN-3c: no Unbekannt when the E locus is known (family fallback)', () => {
it('eef with unknown other loci -> Fuchsschimmel (the reported bug case)', () => {
expect(genotypeToFarbschlag(fromDisplayString('aa C- D- eef Gg Pp spsp --'))).toBe(
'Fuchsschimmel',
)
})
it('ee -> Fuchs family, efef -> a Schimmel (never Unbekannt) even with unknowns', () => {
expect(farbschlagFor(fromDisplayString('a- C- D- ee G- P-')).unknown).toBe(false)
expect(farbschlagFor(fromDisplayString('A- C- D- efef G- P-')).unknown).toBe(false)
})
it('unknown allele no longer blocks a match (wildcard, never Unbekannt)', () => {
// C unknown must not force Unbekannt — it resolves to SOME named variety.
expect(farbschlagFor(fromDisplayString('aa C- DD EE GG PP spsp rere')).unknown).toBe(false)
})
})
describe('GEN-3d: dominance tiebreak for unknown loci', () => {
it('unknown-C reads as full-colour, NOT a c^h/c^chm white', () => {
// 'aa C- DD EE GG PP' -> Schwarz (dominant C reading), never PEW/Hermelin/Himalaya.
const name = genotypeToFarbschlag(fromDisplayString('aa C- DD EE GG PP spsp rere'))
expect(name).toBe('Schwarz')
expect(['Pink Eyed White (PEW)', 'Hermelin', 'Himalaya']).not.toContain(name)
})
it('unknown second marker allele defaults UNMARKED, not Schecke (marker-aware)', () => {
// 'sp-' (one sp + unknown) -> sp/sp -> no Schecke (naive most-dominant would wrongly add it).
expect(genotypeToFarbschlag(fromDisplayString('AA CC DD EE GG PP sp- rere'))).toBe('Agouti')
})
it('still: eef with unknowns -> Fuchsschimmel (family pin unaffected by tiebreak)', () => {
expect(genotypeToFarbschlag(fromDisplayString('aa C- D- eef Gg Pp spsp --'))).toBe(
'Fuchsschimmel',
)
})
})
describe('GEN-3e: C-locus colourpoint naming', () => {
const name = (s: string) => genotypeToFarbschlag(fromDisplayString(s))
it('A- + cchm/cchm -> CP-<base colour> (CP-Silberagouti example)', () => {
expect(name('AA cchmcchm DD EE gg PP spsp rere')).toBe('CP-Silberagouti')
expect(name('AA cchmcchm DD EE GG PP spsp rere')).toBe('CP-Agouti')
})
it('A- + cchm/ch -> CP-<base colour>-Hell (GEN-3g: het gets -Hell suffix)', () => {
expect(name('AA cchmch DD EE GG PP spsp rere')).toBe('CP-Agouti-Hell')
expect(name('AA cchmch DD EE gg PP spsp rere')).toBe('CP-Silberagouti-Hell')
expect(name('AA cchmch DD ee GG PP spsp rere')).toBe('CP-Algierfuchs-Hell')
expect(name('AA cchmch DD ee gg PP spsp rere')).toBe('CP-Polarfuchs-Hell')
expect(name('AA cchmch dd ee GG PP spsp rere')).toBe('CP-Fuchs-Hell')
})
it('aa fixed colourpoint names (Marder/Siam/Zobel/Zobel-Hell)', () => {
expect(name('aa cchmcchm DD EE GG PP spsp rere')).toBe('Marder')
expect(name('aa cchmch DD EE GG PP spsp rere')).toBe('Siam')
expect(name('aa cchmcchm DD EE gg PP spsp rere')).toBe('Zobel')
expect(name('aa cchmch DD EE gg PP spsp rere')).toBe('Zobel-Hell')
})
it('chch stays Hermelin (aa) / Himalaya (A-); full C => no CP', () => {
expect(name('aa chch DD EE GG PP spsp rere')).toBe('Hermelin')
expect(name('AA chch DD EE GG PP spsp rere')).toBe('Himalaya')
expect(name('AA CC DD EE GG PP spsp rere')).toBe('Agouti') // one full C -> no colourpoint
})
it('colourpoint composes with the Schecke modifier', () => {
expect(name('aa cchmcchm DD EE GG PP Spsp rere')).toBe('Marder Schecke')
})
})
describe('GEN-3f: CP catalog reconciled to the breeder CP- naming (matches her live data)', () => {
const name = (s: string) => genotypeToFarbschlag(fromDisplayString(s))
const has = (n: string) => BASE_COLORS.some((e) => e.name === n)
it('every CP- name her data uses exists as a catalog row (import name-match)', () => {
for (const n of [
'CP-Agouti', 'CP-Silberagouti', 'CP-Algierfuchs', 'CP-Polarfuchs',
'CP-Fuchs', 'CP-Fuchs-Hell', 'CP-Blaufuchs', 'CP-Orangeschimmel',
'Marder', 'Siam', 'Zobel', 'Zobel-Hell',
]) {
expect(has(n)).toBe(true)
}
})
it('the removed portal suffix-names are gone (collapsed)', () => {
for (const n of [
'Agouti CP', 'Agouti CP-Hell', 'Silberagouti CP', 'Algierfuchs CP',
'Polarfuchs CP', 'Blaufuchs CP', 'Kohlfuchs CP', 'Kohlfuchsschimmel CP',
'Marder dd', 'Zobel dd', 'Siam (Marder-Hell)', 'Agouti dd CP',
]) {
expect(has(n)).toBe(false)
}
})
it('the engine computes the agouti CP- names her data uses (E-family fallback covers fox/dilute)', () => {
expect(name('AA cchmcchm DD EE GG PP spsp rere')).toBe('CP-Agouti')
expect(name('AA cchmcchm DD EE gg PP spsp rere')).toBe('CP-Silberagouti')
expect(name('AA cchmcchm DD ee GG PP spsp rere')).toBe('CP-Algierfuchs')
expect(name('AA cchmcchm DD ee gg PP spsp rere')).toBe('CP-Polarfuchs')
// dd agouti fox has no dedicated base -> the eFamily fallback yields 'CP-Fuchs'.
expect(name('AA cchmcchm dd ee GG PP spsp rere')).toBe('CP-Fuchs')
// A- cchm efef -> CP-Orangeschimmel (Schimmel base under full C).
expect(name('AA cchmcchm DD efef GG PP spsp rere')).toBe('CP-Orangeschimmel')
})
it('het cchm/ch colourpoints (Siam, Zobel-Hell, CP-*-Hell) resolve to their own names', () => {
const siam = BASE_COLORS.find((e) => e.name === 'Siam')!
const zh = BASE_COLORS.find((e) => e.name === 'Zobel-Hell')!
const cpah = BASE_COLORS.find((e) => e.name === 'CP-Agouti-Hell')!
const cpfh = BASE_COLORS.find((e) => e.name === 'CP-Fuchs-Hell')!
expect(genotypeToFarbschlag(representativeGenotype(siam))).toBe('Siam')
expect(genotypeToFarbschlag(representativeGenotype(zh))).toBe('Zobel-Hell')
expect(genotypeToFarbschlag(representativeGenotype(cpah))).toBe('CP-Agouti-Hell')
expect(genotypeToFarbschlag(representativeGenotype(cpfh))).toBe('CP-Fuchs-Hell')
})
})
describe('GEN-3g: "-Hell" in variety name == cchm/ch het; hom == cchm/cchm', () => {
const name = (s: string) => genotypeToFarbschlag(fromDisplayString(s))
const has = (n: string) => BASE_COLORS.some((e) => e.name === n)
it('all new -Hell het entries exist in catalog', () => {
for (const n of [
'CP-Agouti-Hell', 'CP-Silberagouti-Hell', 'CP-Algierfuchs-Hell',
'CP-Polarfuchs-Hell', 'CP-Orangeschimmel-Hell',
]) {
expect(has(n)).toBe(true)
}
})
it('engine correctly maps het (cchm/ch) to -Hell suffix for all A- bases', () => {
expect(name('AA cchmch DD EE GG PP spsp rere')).toBe('CP-Agouti-Hell')
expect(name('AA cchmch DD EE gg PP spsp rere')).toBe('CP-Silberagouti-Hell')
expect(name('AA cchmch DD ee GG PP spsp rere')).toBe('CP-Algierfuchs-Hell')
expect(name('AA cchmch DD ee gg PP spsp rere')).toBe('CP-Polarfuchs-Hell')
expect(name('AA cchmch dd ee GG PP spsp rere')).toBe('CP-Fuchs-Hell')
expect(name('AA cchmch DD efef GG PP spsp rere')).toBe('CP-Orangeschimmel-Hell')
})
it('hom (cchm/cchm) still maps without -Hell suffix', () => {
expect(name('AA cchmcchm DD EE GG PP spsp rere')).toBe('CP-Agouti')
expect(name('AA cchmcchm DD EE gg PP spsp rere')).toBe('CP-Silberagouti')
expect(name('AA cchmcchm DD efef GG PP spsp rere')).toBe('CP-Orangeschimmel')
})
it('aa non-agouti branch is unchanged (Marder/Siam/Zobel/Zobel-Hell)', () => {
expect(name('aa cchmcchm DD EE GG PP spsp rere')).toBe('Marder')
expect(name('aa cchmch DD EE GG PP spsp rere')).toBe('Siam')
expect(name('aa cchmcchm DD EE gg PP spsp rere')).toBe('Zobel')
expect(name('aa cchmch DD EE gg PP spsp rere')).toBe('Zobel-Hell')
})
})