import { describe, it, expect, beforeEach } from 'vitest';
import { SequenceGenerator, DNA_BASES, RNA_BASES, AMINO_ACIDS, GENETIC_CODE } from '../../src/utils/sequenceUtils';
import { testSequences, expectedResults } from '../fixtures/sequences';
describe('SequenceGenerator', () => {
let generator: SequenceGenerator;
beforeEach(() => {
generator = new SequenceGenerator(12345); // Fixed seed for reproducible tests
});
describe('Constructor', () => {
it('should create generator with fixed seed', () => {
const gen1 = new SequenceGenerator(123);
const gen2 = new SequenceGenerator(123);
const seq1 = gen1.generateRandomDNA(10);
const seq2 = gen2.generateRandomDNA(10);
expect(seq1).toBe(seq2);
});
it('should create generator without seed', () => {
const gen = new SequenceGenerator();
const seq = gen.generateRandomDNA(10);
expect(seq).toMatch(/^[ATGC]+$/);
expect(seq.length).toBe(10);
});
});
describe('generateRandomDNA', () => {
it('should generate DNA of correct length', () => {
const lengths = [1, 10, 100, 1000];
lengths.forEach(length => {
const sequence = generator.generateRandomDNA(length);
expect(sequence.length).toBe(length);
});
});
it('should only contain valid DNA bases', () => {
const sequence = generator.generateRandomDNA(1000);
expect(sequence).toMatch(/^[ATGC]+$/);
});
it('should respect GC content parameter', () => {
const testCases = [
{ gcContent: 0.0, tolerance: 0.1 },
{ gcContent: 0.25, tolerance: 0.1 },
{ gcContent: 0.5, tolerance: 0.1 },
{ gcContent: 0.75, tolerance: 0.1 },
{ gcContent: 1.0, tolerance: 0.1 }
];
testCases.forEach(({ gcContent, tolerance }) => {
const sequence = generator.generateRandomDNA(1000, gcContent);
const actualGC = (sequence.match(/[GC]/g) || []).length / sequence.length;
expect(actualGC).toBeCloseTo(gcContent, 1);
expect(Math.abs(actualGC - gcContent)).toBeLessThan(tolerance);
});
});
it('should generate different sequences without seed', () => {
const gen = new SequenceGenerator();
const seq1 = gen.generateRandomDNA(100);
const seq2 = gen.generateRandomDNA(100);
expect(seq1).not.toBe(seq2);
});
});
describe('generateRandomProtein', () => {
it('should generate protein of correct length', () => {
const lengths = [1, 20, 100, 500];
lengths.forEach(length => {
const sequence = generator.generateRandomProtein(length);
expect(sequence.length).toBe(length);
});
});
it('should only contain valid amino acids', () => {
const sequence = generator.generateRandomProtein(1000);
const validPattern = new RegExp(`^[${AMINO_ACIDS.join('')}]+$`);
expect(sequence).toMatch(validPattern);
});
it('should have reasonable amino acid distribution', () => {
const sequence = generator.generateRandomProtein(2000);
const composition: Record<string, number> = {};
for (const aa of sequence) {
composition[aa] = (composition[aa] || 0) + 1;
}
// Should have most amino acids represented
expect(Object.keys(composition).length).toBeGreaterThan(15);
// No single amino acid should dominate (with some tolerance)
Object.values(composition).forEach(count => {
expect(count / sequence.length).toBeLessThan(0.2);
});
});
});
describe('mutateDNA', () => {
const testSeq = testSequences.dna.medium;
it('should return a string', () => {
const mutated = generator.mutateDNA(testSeq, { substitutionRate: 0.1 });
expect(typeof mutated).toBe('string');
});
it('should only contain valid DNA bases after mutation', () => {
const mutated = generator.mutateDNA(testSeq, {
substitutionRate: 0.5,
insertionRate: 0.1,
deletionRate: 0.1
});
expect(mutated).toMatch(/^[ATGC]+$/);
});
it('should change sequence with high mutation rate', () => {
const mutated = generator.mutateDNA(testSeq, { substitutionRate: 0.5 });
expect(mutated).not.toBe(testSeq);
});
it('should handle insertion mutations', () => {
const mutated = generator.mutateDNA(testSeq, {
substitutionRate: 0,
insertionRate: 0.2
});
expect(mutated.length).toBeGreaterThanOrEqual(testSeq.length);
});
it('should handle deletion mutations', () => {
const mutated = generator.mutateDNA(testSeq, {
substitutionRate: 0,
deletionRate: 0.2
});
expect(mutated.length).toBeLessThanOrEqual(testSeq.length);
expect(mutated.length).toBeGreaterThan(0); // Should not delete entire sequence
});
it('should respect transition bias', () => {
const transitions = ['AG', 'GA', 'CT', 'TC'];
const transversions = ['AC', 'CA', 'AT', 'TA', 'GT', 'TG', 'GC', 'CG'];
let transitionCount = 0;
let transversionCount = 0;
// Run multiple mutations to get statistics
for (let i = 0; i < 100; i++) {
const original = 'A'.repeat(100);
const mutated = generator.mutateDNA(original, {
substitutionRate: 0.1,
transitionBias: 10.0 // Strong bias towards transitions
});
for (let j = 0; j < original.length; j++) {
if (original[j] !== mutated[j]) {
const change = original[j] + mutated[j];
if (transitions.includes(change)) {
transitionCount++;
} else if (transversions.includes(change)) {
transversionCount++;
}
}
}
}
if (transitionCount + transversionCount > 0) {
const ratio = transitionCount / (transversionCount || 1);
expect(ratio).toBeGreaterThan(2); // Should favor transitions
}
});
});
describe('evolveSequence', () => {
const testSeq = testSequences.dna.short;
it('should return array of generations', () => {
const evolution = generator.evolveSequence(testSeq, {
generations: 5,
populationSize: 10,
mutationRate: 0.01
});
expect(Array.isArray(evolution)).toBe(true);
expect(evolution.length).toBe(6); // initial + 5 generations
expect(evolution[0]).toBe(testSeq); // First should be original
});
it('should maintain valid DNA sequences', () => {
const evolution = generator.evolveSequence(testSeq, {
generations: 3,
populationSize: 5,
mutationRate: 0.1
});
evolution.forEach(seq => {
expect(seq).toMatch(/^[ATGC]+$/);
expect(seq.length).toBeGreaterThan(0);
});
});
it('should show changes over generations with mutation', () => {
const evolution = generator.evolveSequence(testSeq, {
generations: 10,
populationSize: 20,
mutationRate: 0.1
});
const finalSeq = evolution[evolution.length - 1];
// With high mutation rate, final should likely be different
// (though not guaranteed due to randomness)
expect(typeof finalSeq).toBe('string');
expect(finalSeq.length).toBeGreaterThan(0);
});
it('should handle selection pressure', () => {
const evolution = generator.evolveSequence(testSeq, {
generations: 5,
populationSize: 10,
mutationRate: 0.1,
selectionPressure: 0.5
});
expect(evolution.length).toBe(6);
evolution.forEach(seq => {
expect(seq).toMatch(/^[ATGC]+$/);
});
});
});
});
describe('Constants', () => {
it('should have correct DNA bases', () => {
expect(DNA_BASES).toEqual(['A', 'T', 'G', 'C']);
});
it('should have correct RNA bases', () => {
expect(RNA_BASES).toEqual(['A', 'U', 'G', 'C']);
});
it('should have correct amino acids', () => {
expect(AMINO_ACIDS).toHaveLength(20);
expect(AMINO_ACIDS).toContain('A');
expect(AMINO_ACIDS).toContain('Y');
expect(AMINO_ACIDS).toContain('W');
});
it('should have complete genetic code', () => {
expect(Object.keys(GENETIC_CODE)).toHaveLength(64); // 4^3 codons
// Check some specific codons
expect(GENETIC_CODE['ATG']).toBe('M'); // Start codon
expect(GENETIC_CODE['TAA']).toBe('*'); // Stop codon
expect(GENETIC_CODE['TTT']).toBe('F'); // Phenylalanine
expect(GENETIC_CODE['GGG']).toBe('G'); // Glycine
});
it('should have all stop codons marked', () => {
const stopCodons = Object.entries(GENETIC_CODE)
.filter(([_, aa]) => aa === '*')
.map(([codon, _]) => codon);
expect(stopCodons).toEqual(['TAA', 'TAG', 'TGA']);
});
});