← Files Biological Sequence & Alignment ViewerARCHIVED FILE

src/views/workspace-export-producers.test.ts

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

↓ Download file

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

import {
  buildAlignedFasta,
  buildSearchHitsTsv,
  buildVisibleRangeSvg,
} from "../msa/exports";
import { buildFastaExport, buildFastqExport } from "../sequence/exports";
import type { SequenceRecord } from "../sequence/types";
import {
  createAlignedFastaWorkspaceProducer,
  createDelimitedWorkspaceProducer,
  createFastaWorkspaceProducer,
  createFastqWorkspaceProducer,
  createJsonWorkspaceProducer,
  createNewickWorkspaceProducer,
  createSearchHitsTsvWorkspaceProducer,
  createVisibleRangeSvgWorkspaceProducer,
} from "./workspace-export-producers";

describe("workspace export producers", () => {
  it("is byte-identical to Sequence and Alignment string exports", async () => {
    const record = {
      description: "demo",
      features: [],
      id: "r1",
      length: 4,
      metadata: {},
      molecule: "dna",
      quality: { ascii: "IIII", phred: [40, 40, 40, 40] },
      sequence: "ACGT",
      sourceLabel: "record",
      topology: "linear",
    } satisfies SequenceRecord;
    const rows = [
      { alignedSequence: "A-", id: "a", label: "a", ungappedLength: 1 },
      { alignedSequence: "AC", id: "b", label: "b", ungappedLength: 2 },
    ];
    await expect(materialize(createFastaWorkspaceProducer([record]))).resolves.toBe(
      buildFastaExport([record]),
    );
    await expect(materialize(createFastqWorkspaceProducer(record))).resolves.toBe(
      buildFastqExport(record),
    );
    await expect(
      materialize(createAlignedFastaWorkspaceProducer(rows)),
    ).resolves.toBe(buildAlignedFasta(rows));
    await expect(
      materialize(
        createVisibleRangeSvgWorkspaceProducer({
          endColumn: 2,
          rows,
          startColumn: 0,
        }),
      ),
    ).resolves.toBe(
      buildVisibleRangeSvg({ endColumn: 2, rows, startColumn: 0 }),
    );
  });

  it("matches JSON.stringify pretty output while streaming large strings", async () => {
    const value = { rows: [{ label: "café", sequence: "A".repeat(100_000) }] };
    await expect(materialize(createJsonWorkspaceProducer(value))).resolves.toBe(
      JSON.stringify(value, null, 2),
    );
  });

  it("streams CSV, TSV, and Newick escaping across bounded fragments", async () => {
    await expect(
      materialize(
        createDelimitedWorkspaceProducer(
          [
            ["name", "value"],
            ['a"b', "line\nbreak"],
          ],
          ",",
        ),
      ),
    ).resolves.toBe('name,value\n"a""b","line\nbreak"');
    await expect(
      materialize(
        createDelimitedWorkspaceProducer([["a\tb", "line\r\nbreak"]], "\t"),
      ),
    ).resolves.toBe("a b\tline break");
    await expect(
      materialize(createNewickWorkspaceProducer("(a:1,b:1)")),
    ).resolves.toBe("(a:1,b:1);\n");
  });

  it("neutralizes formula cells in CSV and TSV without changing strands or numbers", async () => {
    const values = [
      "=1+1",
      "\t=1+1",
      "\r=1+1",
      "\n=1+1",
      "\u200b=1+1",
      "\uff1d1+1",
      "-",
      "+",
      -42,
      "CCNA2_MOUSE/171-297",
    ];

    const csv = await materialize(createDelimitedWorkspaceProducer([values], ","));
    expect(csv).toContain("'=1+1,'\t=1+1");
    expect(csv).toContain('"\'\r=1+1","\'\n=1+1"');
    expect(csv).toContain("'\u200b=1+1,'\uff1d1+1");
    expect(csv.endsWith(",-,+,-42,CCNA2_MOUSE/171-297")).toBe(true);

    const tsv = await materialize(createDelimitedWorkspaceProducer([values], "\t"));
    expect(tsv.split("\t")).toEqual([
      "'=1+1",
      "' =1+1",
      "' =1+1",
      "' =1+1",
      "'\u200b=1+1",
      "'\uff1d1+1",
      "-",
      "+",
      "-42",
      "CCNA2_MOUSE/171-297",
    ]);
  });

  it("keeps sanitized spreadsheet cells in bounded streaming fragments", async () => {
    const value = `=${"A".repeat(12_000)}`;
    for (const delimiter of [",", "\t"] as const) {
      const producer = createDelimitedWorkspaceProducer([[value]], delimiter);
      const fragments: Array<string> = [];
      for await (const fragment of producer()) {
        if (typeof fragment !== "string") {
          throw new Error("Spreadsheet exporter emitted an unexpected binary chunk.");
        }
        fragments.push(fragment);
      }
      expect(fragments.join("")).toBe(`'${value}`);
      expect(Math.max(...fragments.map((fragment) => fragment.length))).toBeLessThanOrEqual(
        4_096,
      );
    }
  });

  it("sanitizes formula and row-injection payloads in streamed motif hits", async () => {
    const hits = [
      {
        alignmentEndColumn: 3,
        alignmentStartColumn: 1,
        orientation: "forward" as const,
        rowId: "public-pfam-row",
        rowLabel: "\ufeff\t=HYPERLINK(\"https://example.invalid\")\r\n@SUM(1)",
        ungappedEndPosition: 3,
        ungappedStartPosition: 2,
      },
    ];

    const result = await materialize(createSearchHitsTsvWorkspaceProducer(hits));
    expect(result).toBe(buildSearchHitsTsv(hits));
    expect(result.split("\n")).toHaveLength(2);
    expect(result.split("\n")[1]?.split("\t")).toHaveLength(6);
    expect(result).toContain("'\ufeff =HYPERLINK");
    expect(result).toContain(") @SUM(1)\tforward\t2\t4\t2\t3");
  });
});

async function materialize(
  producer: () => AsyncIterable<string | Uint8Array>,
): Promise<string> {
  const chunks: Array<Uint8Array> = [];
  for await (const chunk of producer()) {
    chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
  }
  return Buffer.concat(chunks).toString("utf8");
}

SHA-256: 282a98a4c5e0d99d9407aa6e8b36a22ea3095dcf1b4123b8dc19716296ba72eb