← Files Biological Sequence & Alignment ViewerARCHIVED FILE

src/sequence/analysis.test.ts

9.04 KB · Sep 30, 2026 · 23:01 UTC

↓ Download file

import { describe, expect, it } from "vitest";

import {
  COMMON_RESTRICTION_ENZYMES,
  calculateSequenceStatistics,
  designPrimerPairs,
  findOpenReadingFrames,
  findRestrictionSites,
  simulateDigest,
} from "./analysis";

describe("sequence workbench analyses", () => {
  it("computes bounded, explicit nucleotide statistics", () => {
    expect(calculateSequenceStatistics("ACGTNN", "nucleic-acid")).toEqual({
      ambiguousCount: 2,
      gcFraction: 2 / 6,
      length: 6,
      molecule: "nucleic-acid",
      nFraction: 2 / 6,
      symbolCounts: { A: 1, C: 1, G: 1, N: 2, T: 1 },
    });
  });

  it("finds ORFs on both strands with table-specific stops", () => {
    const sequence = `ATG${"GCT".repeat(5)}TGA`;
    const standard = findOpenReadingFrames({
      geneticCodeId: 1,
      minAminoAcids: 5,
      sequence,
    });
    const mitochondrial = findOpenReadingFrames({
      geneticCodeId: 2,
      includePartial: true,
      minAminoAcids: 5,
      sequence,
      strands: "+",
    });

    expect(standard.items[0]).toMatchObject({
      aminoAcidLength: 6,
      completeStop: true,
      frame: 1,
      geneticCodeId: 1,
      start: 1,
      strand: "+",
    });
    expect(mitochondrial.items[0]).toMatchObject({
      completeStop: false,
      geneticCodeId: 2,
      translation: "MAAAAAW",
    });
  });

  it("maps reverse-strand ORFs back into source coordinates", () => {
    const reverseOrf = findOpenReadingFrames({
      minAminoAcids: 2,
      sequence: "TTATTTCAT",
      strands: "-",
    }).items[0];

    expect(reverseOrf).toMatchObject({
      end: 9,
      start: 1,
      strand: "-",
      translation: "MK",
    });
  });

  it("finds circular origin-spanning restriction sites without palindrome duplicates", () => {
    const sites = findRestrictionSites({
      circular: true,
      enzymes: [
        {
          cutBottom: 5,
          cutTop: 1,
          id: "ecori",
          name: "EcoRI",
          recognitionSequence: "GAATTC",
        },
      ],
      sequence: "AATTCG",
    });

    expect(sites).toEqual({
      items: [
        expect.objectContaining({
          end: 5,
          enzymeName: "EcoRI",
          start: 6,
          wrapsOrigin: true,
        }),
      ],
      truncated: false,
    });
  });

  it("does not wrap off-end Type IIS cuts on linear records", () => {
    const linear = findRestrictionSites({
      circular: false,
      enzymes: [
        {
          cutBottom: 11,
          cutTop: 7,
          id: "bsai",
          name: "BsaI",
          recognitionSequence: "GGTCTC",
        },
      ],
      sequence: "GGTCTC",
    });
    const circular = findRestrictionSites({
      circular: true,
      enzymes: [
        {
          cutBottom: 11,
          cutTop: 7,
          id: "bsai",
          name: "BsaI",
          recognitionSequence: "GGTCTC",
        },
      ],
      sequence: "GGTCTC",
    });

    expect(linear.items[0]).toMatchObject({ cutBottom: 12, cutTop: 8 });
    expect(circular.items[0]).toMatchObject({ cutBottom: 6, cutTop: 2 });
    expect(
      simulateDigest({
        circular: false,
        sequenceLength: 6,
        sites: linear.items,
      }),
    ).toEqual([{ end: 6, length: 6, start: 1, wrapsOrigin: false }]);
  });

  it.each([
    { enzymeName: "BsaI", forwardMotif: "GGTCTC", reverseMotif: "GAGACC" },
    { enzymeName: "BsmBI", forwardMotif: "CGTCTC", reverseMotif: "GAGACG" },
  ])(
    "mirrors asymmetric $enzymeName cuts upstream on the reverse strand",
    ({ enzymeName, forwardMotif, reverseMotif }) => {
      const enzymes = COMMON_RESTRICTION_ENZYMES.filter(
        ({ name }) => name === enzymeName,
      );
      const forward = findRestrictionSites({
        enzymes,
        sequence: `AAAAAA${forwardMotif}AAAAAAAAAAA`,
      });
      const reverse = findRestrictionSites({
        enzymes,
        sequence: `AAAAAA${reverseMotif}AAAAAAAAAAA`,
      });

      expect(forward.items).toEqual([
        expect.objectContaining({
          cutBottom: 18,
          cutTop: 14,
          end: 12,
          start: 7,
          strand: "+",
        }),
      ]);
      expect(reverse.items).toEqual([
        expect.objectContaining({
          cutBottom: 6,
          cutTop: 2,
          end: 12,
          start: 7,
          strand: "-",
        }),
      ]);
    },
  );

  it("reports the genuine RefSeq NC_005816.1 reverse BsaI cuts at 338/342", () => {
    // Immutable public RefSeq plasmid NC_005816.1, first 350 nucleotides:
    // biopython/biopython@c9489604d1d9607602ca9199a3852c1219ed330f,
    // Tests/GenBank/NC_005816.fna.
    const publicRefSeqPlasmid = [
      "TGTAACGAACGGTGCAATAGTGATCCACACCCAACGCCTGAAATCAGATCCAGGGGGTAATCTGCTCTCC",
      "TGATTCAGGAGAGTTTATGGTCACTTTTGAGACAGTTATGGAAATTAAAATCCTGCACAAGCAGGGAATG",
      "AGTAGCCGGGCGATTGCCAGAGAACTGGGGATCTCCCGCAATACCGTTAAACGTTATTTGCAGGCAAAAT",
      "CTGAGCCGCCAAAATATACGCCGCGACCTGCTGTTGCTTCACTCCTGGATGAATACCGGGATTATATTCG",
      "TCAACGCATCGCCGATGCTCATCCTTACAAAATCCCGGCAACGGTAATCGCTCGCGAGATCAGAGACCAG",
    ].join("");
    const sites = findRestrictionSites({
      enzymes: COMMON_RESTRICTION_ENZYMES.filter(({ id }) => id === "bsai"),
      sequence: publicRefSeqPlasmid,
    });

    expect(sites.items).toContainEqual(
      expect.objectContaining({
        cutBottom: 342,
        cutTop: 338,
        end: 348,
        enzymeName: "BsaI",
        start: 343,
        strand: "-",
      }),
    );
  });

  it("wraps mirrored reverse Type IIS cuts only on circular records", () => {
    const enzymes = COMMON_RESTRICTION_ENZYMES.filter(
      ({ id }) => id === "bsai",
    );
    const linear = findRestrictionSites({
      circular: false,
      enzymes,
      sequence: "GAGACC",
    });
    const circular = findRestrictionSites({
      circular: true,
      enzymes,
      sequence: "GAGACC",
    });
    const originSpanning = findRestrictionSites({
      circular: true,
      enzymes,
      sequence: "ACCGAG",
    });

    expect(linear.items[0]).toMatchObject({
      cutBottom: 0,
      cutTop: -4,
      strand: "-",
    });
    expect(circular.items[0]).toMatchObject({
      cutBottom: 6,
      cutTop: 2,
      strand: "-",
    });
    expect(originSpanning.items[0]).toMatchObject({
      cutBottom: 3,
      cutTop: 5,
      end: 3,
      start: 4,
      strand: "-",
      wrapsOrigin: true,
    });
    expect(
      simulateDigest({
        circular: false,
        sequenceLength: 6,
        sites: linear.items,
      }),
    ).toEqual([{ end: 6, length: 6, start: 1, wrapsOrigin: false }]);
  });

  it("preserves ambiguous recognition and RNA normalization on reverse cuts", () => {
    const sites = findRestrictionSites({
      enzymes: [
        {
          cutBottom: 11,
          cutTop: 7,
          id: "ambiguous-type-iis",
          name: "Ambiguous Type IIS",
          recognitionSequence: "GGNCTC",
        },
      ],
      sequence: "UUUUUUGAGACCUUUUUU",
    });

    expect(sites.items).toEqual([
      expect.objectContaining({
        cutBottom: 6,
        cutTop: 2,
        end: 12,
        start: 7,
        strand: "-",
      }),
    ]);
  });

  it("simulates linear and circular digest fragment boundaries", () => {
    const sites = [
      {
        cutBottom: 3,
        cutTop: 3,
        end: 4,
        enzymeId: "x",
        enzymeName: "X",
        recognitionSequence: "AAAA",
        start: 1,
        strand: "+" as const,
        wrapsOrigin: false,
      },
      {
        cutBottom: 8,
        cutTop: 8,
        end: 9,
        enzymeId: "x",
        enzymeName: "X",
        recognitionSequence: "AAAA",
        start: 6,
        strand: "+" as const,
        wrapsOrigin: false,
      },
    ];

    expect(
      simulateDigest({ circular: false, sequenceLength: 10, sites }),
    ).toEqual([
      { end: 2, length: 2, start: 1, wrapsOrigin: false },
      { end: 7, length: 5, start: 3, wrapsOrigin: false },
      { end: 10, length: 3, start: 8, wrapsOrigin: false },
    ]);
    expect(
      simulateDigest({ circular: true, sequenceLength: 10, sites }),
    ).toEqual([
      { end: 7, length: 5, start: 3, wrapsOrigin: false },
      { end: 2, length: 5, start: 8, wrapsOrigin: true },
    ]);
  });

  it("designs reproducible primer pairs that bracket the selected target", () => {
    let seed = 17;
    const sequence = Array.from({ length: 700 }, () => {
      seed = (seed * 48_271) % 2_147_483_647;
      return "ACGT"[seed % 4];
    }).join("");
    const pairs = designPrimerPairs({
      minProductLength: 60,
      sequence,
      targetEnd: 327,
      targetStart: 321,
    });

    expect(pairs.length).toBeGreaterThan(0);
    expect(pairs[0]).toMatchObject({
      forward: expect.objectContaining({ strand: "+" }),
      reverse: expect.objectContaining({ strand: "-" }),
    });
    expect(pairs[0]?.productStart).toBeLessThanOrEqual(321);
    expect(pairs[0]?.productEnd).toBeGreaterThanOrEqual(327);
  });

  it("caps ORF result cardinality and reports truncation", () => {
    const result = findOpenReadingFrames({
      maxResults: 2,
      minAminoAcids: 1,
      sequence: "ATGATGTAAATGTAA",
      strands: "+",
    });
    expect(result.items).toHaveLength(2);
    expect(result.truncated).toBe(true);
  });
});

SHA-256: 631abd067e67b7cdc1b68d7ee86cefadb5bf9cbdceecc7d4ffb40b2f8bf09af9