← Files Biological Sequence & Alignment ViewerARCHIVED FILE
src/sequence/orientation.test.ts
5.51 KB · Sep 30, 2026 · 23:01 UTC
import { describe, expect, it } from "vitest";
import { parseSequenceDocument } from "./parser";
import {
displaySelectionToSource,
orientSearchHit,
orientSequenceRecord,
sourceCoordinateToDisplay,
sourceRangeToDisplay,
} from "./orientation";
describe("sequence orientation projection", () => {
it("round-trips one-based inclusive coordinates", () => {
expect(sourceCoordinateToDisplay(1, 10, "reverse-complement")).toBe(10);
expect(sourceRangeToDisplay({ end: 4, start: 2 }, 10, "reverse-complement")).toEqual({
end: 9,
start: 7,
});
expect(
displaySelectionToSource(
{ end: 9, recordId: "r", start: 7 },
10,
"reverse-complement",
),
).toEqual({ end: 4, recordId: "r", start: 2 });
});
it("reverse-complements sequence, quality, features, and strands without mutating source", () => {
const document = parseSequenceDocument({
contents: `LOCUS ORIENT 8 bp DNA linear
ACCESSION ORIENT1
FEATURES Location/Qualifiers
gene 2..5
/gene="demo"
ORIGIN
1 atgccgta
//`,
fileName: "orientation.gb",
});
const record = document.records[0]!;
const oriented = orientSequenceRecord(record, "reverse-complement");
expect(oriented.sequence).toBe("TACGGCAT");
expect(oriented.features[0]).toMatchObject({
end: 7,
start: 4,
strand: "-",
});
expect(record.sequence).toBe("ATGCCGTA");
});
it("projects genuine RNAcentral RNA with uracil instead of introducing thymine", () => {
// Public RNAcentral URS0000D6941A from immutable R2DT fixture
// a1ca674f245e4dc13838e346b8e03295c2130d0c/examples/RF02976.fasta.
const document = parseSequenceDocument({
contents: `>URS0000D6941A
CCGAGAUAGGGGUACAAAUCCUGCCAAUGGCGUUUCCCUGGACCUUAGCGAUGAAGGCAUUUCAAAGCCUGAGCAACGAC
UCUCGGCGUCAGAUGGGGAGAGUGCCAGACGUGGUGUCCCCGCUGAC`,
fileName: "RF02976.fasta",
});
const record = document.records[0]!;
const oriented = orientSequenceRecord(record, "reverse-complement");
expect(record.molecule).toBe("rna");
expect(oriented.molecule).toBe("rna");
expect(oriented.sequence.slice(-12)).toBe("CCCCUAUCUCGG");
expect(oriented.sequence).not.toContain("T");
expect(record.sequence.slice(0, 12)).toBe("CCGAGAUAGGGG");
});
it("preserves RNA IUPAC ambiguity and measured quality orientation", () => {
// Explicitly synthetic negative/edge-case control, not a biological read.
const document = parseSequenceDocument({
contents: "@QA-SYNTHETIC-IUPAC-RNA-READ\nAUGCRYN\n+\n!#%')+-\n",
fileName: "rna.fastq",
});
const record = document.records[0]!;
const oriented = orientSequenceRecord(record, "reverse-complement");
expect(record.molecule).toBe("rna");
expect(oriented.sequence).toBe("NRYGCAU");
expect(oriented.quality?.ascii).toBe("-+)'%#!");
expect(oriented.quality?.phred).toEqual(
[...record.quality!.phred].reverse(),
);
});
it("projects search-hit coordinates and orientation", () => {
expect(
orientSearchHit(
{ end: 5, orientation: "forward", recordId: "r", start: 3 },
10,
"reverse-complement",
),
).toEqual({
end: 8,
orientation: "reverse-complement",
recordId: "r",
start: 6,
});
});
it("keeps source traces only in their original orientation", () => {
// Synthetic trace control; samples are not experimental evidence.
const document = parseSequenceDocument({
contents: ">QA-SYNTHETIC-TRACE\nATGC\n",
fileName: "synthetic-trace.fasta",
});
const record = document.records[0]!;
record.chromatogram = {
channels: {
A: [1, 2, 3, 4],
C: [2, 3, 4, 5],
G: [3, 4, 5, 6],
T: [4, 5, 6, 7],
},
format: "abif",
peakLocations: [0, 1, 2, 3],
quality: [20, 30, 40, 50],
qualityEncoding: "phred",
sampleCount: 4,
};
const source = structuredClone(record);
expect(orientSequenceRecord(record, "forward")).toBe(record);
const reversed = orientSequenceRecord(record, "reverse-complement");
expect(reversed.sequence).toBe("GCAT");
expect(Object.hasOwn(reversed, "chromatogram")).toBe(false);
expect(reversed.quality).toBeUndefined();
expect(record).toEqual(source);
});
it("preserves forward compound order and round-trips reverse-strand selections", () => {
const source = {
end: 2,
recordId: "r",
segments: [
{ end: 12, start: 10 },
{ end: 7, start: 5 },
{ end: 2, start: 1 },
],
start: 10,
};
expect(displaySelectionToSource(source, 12, "forward")).toBe(source);
const reversed = displaySelectionToSource(source, 12, "reverse-complement");
expect(reversed.segments).toEqual([
{ end: 12, start: 11 },
{ end: 8, start: 6 },
{ end: 3, start: 1 },
]);
expect(
displaySelectionToSource(reversed, 12, "reverse-complement"),
).toEqual(source);
});
it("round-trips both segments of an origin-spanning selection", () => {
const source = {
end: 2,
recordId: "r",
segments: [
{ end: 10, start: 8 },
{ end: 2, start: 1 },
],
start: 8,
};
const displayed = displaySelectionToSource(
source,
10,
"reverse-complement",
);
expect(displayed.segments).toEqual([
{ end: 10, start: 9 },
{ end: 3, start: 1 },
]);
expect(
displaySelectionToSource(displayed, 10, "reverse-complement"),
).toEqual(source);
});
});
SHA-256: 5f38f94e6ec47a3ff3ba3898969855b9428c8863edae92cd502ad852f5cf7c82