126 lines
3.9 KiB
TypeScript
126 lines
3.9 KiB
TypeScript
|
|
import {
|
||
|
|
areArchNeighbors,
|
||
|
|
FDI_TOOTH_IDS,
|
||
|
|
isFdiTooth,
|
||
|
|
sameArch,
|
||
|
|
teethBetweenInclusive,
|
||
|
|
toFdi,
|
||
|
|
} from './fdi';
|
||
|
|
|
||
|
|
describe('FDI geometry', () => {
|
||
|
|
describe('toFdi — the patient-right convention', () => {
|
||
|
|
// A mirrored quadrant produces a *valid* code for the wrong tooth, so no schema
|
||
|
|
// check can catch it. These four cases are the guard.
|
||
|
|
it('maps upper + patient right to quadrant 1', () => {
|
||
|
|
expect(toFdi('upper', 'patient_right', 6)).toBe('16');
|
||
|
|
expect(toFdi('upper', 'patient_right', 1)).toBe('11');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('maps upper + patient left to quadrant 2', () => {
|
||
|
|
expect(toFdi('upper', 'patient_left', 6)).toBe('26');
|
||
|
|
expect(toFdi('upper', 'patient_left', 8)).toBe('28');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('maps lower + patient left to quadrant 3', () => {
|
||
|
|
expect(toFdi('lower', 'patient_left', 6)).toBe('36');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('maps lower + patient right to quadrant 4', () => {
|
||
|
|
expect(toFdi('lower', 'patient_right', 6)).toBe('46');
|
||
|
|
expect(toFdi('lower', 'patient_right', 8)).toBe('48');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('never clamps an out-of-range position', () => {
|
||
|
|
expect(toFdi('upper', 'patient_right', 9)).toBeNull();
|
||
|
|
expect(toFdi('upper', 'patient_right', 0)).toBeNull();
|
||
|
|
expect(toFdi('upper', 'patient_right', -1)).toBeNull();
|
||
|
|
expect(toFdi('upper', 'patient_right', 1.5)).toBeNull();
|
||
|
|
expect(toFdi('upper', 'patient_right', Number.NaN)).toBeNull();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('isFdiTooth', () => {
|
||
|
|
it('accepts all 32 permanent teeth', () => {
|
||
|
|
expect(FDI_TOOTH_IDS.size).toBe(32);
|
||
|
|
for (const tooth of FDI_TOOTH_IDS) expect(isFdiTooth(tooth)).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('rejects deciduous teeth — the chart has no primary dentition', () => {
|
||
|
|
for (const tooth of ['51', '55', '61', '71', '85']) {
|
||
|
|
expect(isFdiTooth(tooth)).toBe(false);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('rejects garbage', () => {
|
||
|
|
for (const value of [
|
||
|
|
'',
|
||
|
|
'1',
|
||
|
|
'19',
|
||
|
|
'10',
|
||
|
|
'29',
|
||
|
|
'99',
|
||
|
|
14,
|
||
|
|
null,
|
||
|
|
undefined,
|
||
|
|
{},
|
||
|
|
]) {
|
||
|
|
expect(isFdiTooth(value)).toBe(false);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('adjacency', () => {
|
||
|
|
it('treats neighbours within a quadrant as adjacent', () => {
|
||
|
|
expect(areArchNeighbors('14', '15')).toBe(true);
|
||
|
|
expect(areArchNeighbors('15', '14')).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('treats the midline pairs as adjacent', () => {
|
||
|
|
expect(areArchNeighbors('11', '21')).toBe(true);
|
||
|
|
expect(areArchNeighbors('41', '31')).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('rejects non-neighbours and cross-arch pairs', () => {
|
||
|
|
expect(areArchNeighbors('14', '16')).toBe(false);
|
||
|
|
expect(areArchNeighbors('18', '28')).toBe(false);
|
||
|
|
expect(areArchNeighbors('14', '44')).toBe(false);
|
||
|
|
expect(areArchNeighbors('14', '14')).toBe(false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('sameArch', () => {
|
||
|
|
it('groups by arch, not by quadrant', () => {
|
||
|
|
expect(sameArch('18', '28')).toBe(true);
|
||
|
|
expect(sameArch('48', '38')).toBe(true);
|
||
|
|
expect(sameArch('18', '48')).toBe(false);
|
||
|
|
expect(sameArch('14', '99')).toBe(false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('teethBetweenInclusive', () => {
|
||
|
|
it('returns the span in arch order regardless of argument order', () => {
|
||
|
|
expect(teethBetweenInclusive('14', '16')).toEqual(['16', '15', '14']);
|
||
|
|
expect(teethBetweenInclusive('16', '14')).toEqual(['16', '15', '14']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('spans the midline', () => {
|
||
|
|
expect(teethBetweenInclusive('12', '22')).toEqual([
|
||
|
|
'12',
|
||
|
|
'11',
|
||
|
|
'21',
|
||
|
|
'22',
|
||
|
|
]);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns a single tooth for identical endpoints', () => {
|
||
|
|
expect(teethBetweenInclusive('14', '14')).toEqual(['14']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns null across arches or for unknown teeth', () => {
|
||
|
|
expect(teethBetweenInclusive('14', '44')).toBeNull();
|
||
|
|
expect(teethBetweenInclusive('14', '99')).toBeNull();
|
||
|
|
expect(teethBetweenInclusive('99', '14')).toBeNull();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|