← Files Biological Sequence & Alignment ViewerARCHIVED FILE
src/export-actions-workspace.test.tsx
19.6 KB · Sep 30, 2026 · 23:01 UTC
import { cleanup, render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { IntlProvider } from "react-intl";
import { afterEach, describe, expect, it, vi } from "vitest";
import { MsaExportActions } from "./msa/export-actions";
import { ExportActions } from "./sequence/export-actions";
import { parseSequenceDocument } from "./sequence/parser";
import type {
PreparedSequenceWorkspaceArtifact,
SequenceWorkspaceArtifactPublisher,
} from "./views/workbench-persistence";
afterEach(() => {
cleanup();
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
describe("workspace export action provenance", () => {
it("uses the live sequence workbench revision", async () => {
const publisher = createPublisher("derived.fasta");
const document = parseSequenceDocument({
contents: ">demo\nACGT\n",
fileName: "demo.fasta",
});
render(
<ExportActions
document={document}
publishWorkspaceArtifact={publisher}
record={document.records[0]}
sourceRevision={7}
/>,
);
await userEvent.click(
screen.getByRole("button", { name: "Publish record FASTA" }),
);
const save = await screen.findByRole("button", { name: "Save" });
await waitFor(() => expect(save).toBeEnabled());
await userEvent.click(save);
await waitFor(() => expect(publisher).toHaveBeenCalledOnce());
expect(publisher.mock.calls[0]![0].provenance.sourceRevision).toBe(7);
});
it("uses the live alignment workbench revision", async () => {
const publisher = createPublisher("visible.afa");
render(
<IntlProvider locale="en" messages={{}}>
<MsaExportActions
hits={[]}
publishWorkspaceArtifact={publisher}
referenceLabel="reference"
referenceSequence="ACGT"
rows={[
{
alignedSequence: "ACGT",
id: "reference",
label: "reference",
ungappedLength: 4,
},
]}
selectedColumnRange={null}
sourceRevision={11}
visibleColumnEnd={4}
visibleColumnStart={1}
/>
</IntlProvider>,
);
await userEvent.click(
screen.getByRole("button", { name: "Publish visible FASTA" }),
);
const save = await screen.findByRole("button", { name: "Save" });
await waitFor(() => expect(save).toBeEnabled());
await userEvent.click(save);
await waitFor(() => expect(publisher).toHaveBeenCalledOnce());
expect(publisher.mock.calls[0]![0].provenance.sourceRevision).toBe(11);
});
it("offers only source-bound workspace exports in Sequence mode", () => {
const publisher = createPublisher("derived.fasta");
const document = parseSequenceDocument({
contents: ">demo\nACGT\n",
fileName: "demo.fasta",
});
render(
<ExportActions
document={document}
publishWorkspaceArtifact={publisher}
record={document.records[0]!}
sourceRevision={2}
/>,
);
expect(
screen.getByRole("button", { name: "Publish record FASTA" }),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Publish all FASTA" }),
).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Export record FASTA" }),
).not.toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Export all FASTA" }),
).not.toBeInTheDocument();
});
it("streams exactly the visible Alignment rows through an authorized workspace export", async () => {
const publisher = createPublisher("msa-visible-rows.afa");
render(
<IntlProvider locale="en" messages={{}}>
<MsaExportActions
hits={[]}
publishWorkspaceArtifact={publisher}
referenceLabel="visible"
referenceSequence="AC-T"
rows={[
{
alignedSequence: "AC-T",
id: "visible",
label: "visible",
ungappedLength: 3,
},
{
alignedSequence: "A-GT",
id: "filtered",
label: "filtered",
ungappedLength: 3,
},
]}
selectedColumnRange={null}
sourceRevision={5}
visibleColumnEnd={4}
visibleColumnStart={0}
/>
</IntlProvider>,
);
expect(
screen.queryByRole("button", { name: "Download visible FASTA" }),
).not.toBeInTheDocument();
await publishVisibleArtifact("Publish visible FASTA");
await waitFor(() => expect(publisher).toHaveBeenCalledOnce());
const artifact = publisher.mock.calls[0]![0];
expect(artifact).toEqual(
expect.objectContaining({
format: "aligned-fasta",
name: "msa-visible-rows.afa",
provenance: expect.objectContaining({ sourceRevision: 5 }),
}),
);
expect(artifact).not.toHaveProperty("content");
const chunks: Array<string> = [];
for await (const chunk of artifact.createChunks!()) {
chunks.push(
typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk),
);
}
expect(chunks.join("")).toBe(">visible\nAC-T\n>filtered\nA-GT");
});
it("reports unavailable workspace exports without advertising blocked downloads", () => {
const document = parseSequenceDocument({
contents: ">demo\nACGT\n",
fileName: "demo.fasta",
});
render(
<ExportActions
document={document}
record={document.records[0]!}
sourceRevision={0}
/>,
);
expect(
screen.getByText("Workspace export is unavailable for this viewer."),
).toHaveAttribute("role", "status");
expect(
screen.queryByRole("button", { name: /export|publish/iu }),
).toBeNull();
});
it("reports unavailable Alignment exports while retaining manual copy controls", () => {
renderAlignmentActions(undefined, 0);
expect(
screen.getByText("Workspace export is unavailable for this viewer."),
).toHaveAttribute("role", "status");
expect(
screen.getByRole("button", { name: "Copy visible rows FASTA" }),
).toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Download visible FASTA" }),
).not.toBeInTheDocument();
});
it("confirms a Sequence clipboard write only after the host accepts it", async () => {
const writeText = vi.fn(async () => undefined);
stubClipboard(writeText);
const document = parseSequenceDocument({
contents: ">demo\nACGT\n",
fileName: "demo.fasta",
});
render(
<ExportActions
document={document}
publishWorkspaceArtifact={createPublisher("demo.fasta")}
record={document.records[0]!}
sourceRevision={0}
/>,
);
await userEvent.click(
screen.getByRole("button", { name: "Copy record FASTA" }),
);
expect(writeText).toHaveBeenCalledWith(">demo\nACGT");
expect(await screen.findByRole("status")).toHaveTextContent(
"Copied to clipboard.",
);
expect(
screen.queryByRole("textbox", { name: "Sequence text to copy manually" }),
).not.toBeInTheDocument();
});
it("uses a successful gesture-scoped legacy clipboard fallback", async () => {
stubClipboard(
vi.fn(async () => Promise.reject(new Error("Permission denied"))),
);
const execCommand = vi.fn(() => true);
vi.stubGlobal("document", Object.assign(document, { execCommand }));
renderAlignmentActions(createPublisher("visible.afa"), 0);
await userEvent.click(
screen.getByRole("button", { name: "Copy visible rows FASTA" }),
);
await waitFor(() => expect(execCommand).toHaveBeenCalledWith("copy"));
expect(await screen.findByRole("status")).toHaveTextContent(
"Copied to clipboard.",
);
expect(
screen.queryByRole("textbox", {
name: "Alignment text to copy manually",
}),
).not.toBeInTheDocument();
});
it.each([
["Sequence", "Copy record FASTA", "Sequence text to copy manually"],
["Alignment", "Copy visible rows FASTA", "Alignment text to copy manually"],
] as const)(
"exposes exact selectable %s text when clipboard access is denied",
async (mode, action, accessibleName) => {
stubClipboard(
vi.fn(async () => Promise.reject(new Error("Permission denied"))),
);
const execCommand = vi.fn(() => false);
vi.stubGlobal("document", Object.assign(document, { execCommand }));
if (mode === "Sequence") {
const document = parseSequenceDocument({
contents: ">demo\nACGT\n",
fileName: "demo.fasta",
});
render(
<ExportActions
document={document}
publishWorkspaceArtifact={createPublisher("demo.fasta")}
record={document.records[0]!}
sourceRevision={0}
/>,
);
} else {
renderAlignmentActions(createPublisher("visible.afa"), 0);
}
await userEvent.click(screen.getByRole("button", { name: action }));
expect(await screen.findByRole("status")).toHaveTextContent(
"Clipboard access is unavailable.",
);
const field = screen.getByRole("textbox", { name: accessibleName });
expect(field).toHaveValue(
mode === "Sequence" ? ">demo\nACGT" : ">reference\nACGT",
);
expect(field).toHaveAttribute("readonly");
expect(field).toHaveFocus();
expect(
screen.queryByText("Copied to clipboard."),
).not.toBeInTheDocument();
},
);
it("rejects oversized manual-copy fallbacks before allocating a selection field", async () => {
stubClipboard(
vi.fn(async () => Promise.reject(new Error("Permission denied"))),
);
const execCommand = vi.fn(() => true);
vi.stubGlobal("document", Object.assign(document, { execCommand }));
const sequenceDocument = parseSequenceDocument({
contents: `>large\n${"A".repeat(128 * 1_024 + 1)}\n`,
fileName: "large.fasta",
});
render(
<ExportActions
document={sequenceDocument}
publishWorkspaceArtifact={createPublisher("large.fasta")}
record={sequenceDocument.records[0]!}
sourceRevision={0}
/>,
);
await userEvent.click(
screen.getByRole("button", { name: "Copy record FASTA" }),
);
expect(await screen.findByRole("alert")).toHaveTextContent(
"This selection is too large to display for manual copying.",
);
expect(execCommand).not.toHaveBeenCalled();
expect(
screen.queryByRole("textbox", { name: "Sequence text to copy manually" }),
).not.toBeInTheDocument();
});
it("publishes a complete source PDF through the authenticated native backend", async () => {
const publisher = createPublisher("sequence.pdf");
publisher.supportsNativeRichGeneration = true;
const document = parseSequenceDocument({
contents: ">demo\nACGT\n",
fileName: "demo.fasta",
});
render(
<ExportActions
document={document}
publishWorkspaceArtifact={publisher}
record={document.records[0]}
sourceRevision={0}
/>,
);
await publishVisibleArtifact("Publish source PDF");
await waitFor(() => expect(publisher).toHaveBeenCalledOnce());
expect(publisher.mock.calls[0]![0]).toEqual(
expect.objectContaining({
format: "pdf",
mediaType: "application/pdf",
name: "sequence.pdf",
serverGeneration: { compression: "none", kind: "native-rich" },
}),
);
expect(publisher.mock.calls[0]![0]).not.toHaveProperty("content");
expect(publisher.mock.calls[0]![0]).not.toHaveProperty("createChunks");
});
it("preserves source-bound compound coordinates, strand, and GTF qualifiers", async () => {
const publisher = createPublisher("annotations.gtf");
publisher.supportsNativeRichGeneration = true;
const document = annotatedSequenceDocument();
const record = document.records[0]!;
render(
<ExportActions
document={document}
publishWorkspaceArtifact={publisher}
record={record}
sourceRevision={0}
/>,
);
await publishVisibleArtifact("Publish source GTF");
await waitFor(() => expect(publisher).toHaveBeenCalledOnce());
expect(publisher.mock.calls[0]![0]).toEqual(
expect.objectContaining({
format: "gtf",
mediaType: "text/x-gtf",
serverGeneration: {
compression: "none",
kind: "native-rich",
records: expect.arrayContaining([
expect.objectContaining({
end1: 3,
id: record.features[0]?.id,
kind: "misc_feature",
metadata: {
gene_id: "GENE1",
gene_name: "GENE1",
transcript_id: "TX1",
},
reference: record.id,
start1: 1,
strand: "-",
}),
expect.objectContaining({
end1: 9,
id: record.features[0]?.id,
reference: record.id,
start1: 7,
strand: "-",
}),
]),
},
}),
);
expect(publisher.mock.calls[0]![0].serverGeneration?.records).toHaveLength(
2,
);
});
it("fails closed for remote or incompletely inventoried source annotations", () => {
const publisher = createPublisher("annotations.gtf");
publisher.supportsNativeRichGeneration = true;
const remote = annotatedSequenceDocument();
remote.records[0]!.features[0]!.segments![0]!.remoteAccession = "OTHER.1";
const { rerender } = render(
<ExportActions
document={remote}
publishWorkspaceArtifact={publisher}
record={remote.records[0]!}
sourceRevision={0}
/>,
);
expect(
screen.queryByRole("button", { name: "Publish source GTF" }),
).not.toBeInTheDocument();
const incomplete = annotatedSequenceDocument();
incomplete.recordInventory = {
materializedCount: 1,
totalCount: 2,
truncated: true,
};
rerender(
<ExportActions
document={incomplete}
publishWorkspaceArtifact={publisher}
record={incomplete.records[0]!}
sourceRevision={0}
/>,
);
expect(
screen.queryByRole("button", { name: "Publish source GTF" }),
).not.toBeInTheDocument();
});
it("never routes edited sequence state to whole-source native exports", () => {
const publisher = createPublisher("sequence.pdf");
publisher.supportsNativeRichGeneration = true;
const document = annotatedSequenceDocument();
render(
<ExportActions
document={document}
publishWorkspaceArtifact={publisher}
record={document.records[0]!}
sourceRevision={1}
/>,
);
expect(
screen.queryByRole("button", { name: "Publish source PDF" }),
).not.toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Publish source GTF" }),
).not.toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Publish record FASTA" }),
).toBeInTheDocument();
});
it.each([
["A3M", "a3m", "alignment.a3m"],
["CLUSTAL", "clustal", "alignment.aln"],
["Stockholm", "stockholm", "alignment.sto"],
["PDF", "pdf", "alignment.pdf"],
] as const)(
"publishes complete source %s alignment through the native backend",
async (label, format, name) => {
const publisher = createPublisher(name);
publisher.supportsNativeRichGeneration = true;
renderAlignmentActions(publisher, 0);
await publishVisibleArtifact(`Publish source ${label}`);
await waitFor(() => expect(publisher).toHaveBeenCalledOnce());
expect(publisher.mock.calls[0]![0]).toEqual(
expect.objectContaining({
format,
name,
provenance: expect.objectContaining({
parameters: { kind: `source-${format}`, scope: "all" },
sourceRevision: 0,
}),
serverGeneration: { compression: "none", kind: "native-rich" },
}),
);
expect(publisher.mock.calls[0]![0]).not.toHaveProperty("content");
},
);
it("retains exact visible alignment exports when native source access is unavailable", () => {
const publisher = createPublisher("visible.afa");
renderAlignmentActions(publisher, 0);
expect(
screen.queryByRole("button", { name: "Publish source Stockholm" }),
).not.toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Publish visible FASTA" }),
).toBeInTheDocument();
});
it("never routes edited alignment state to whole-source native exports", () => {
const publisher = createPublisher("alignment.sto");
publisher.supportsNativeRichGeneration = true;
renderAlignmentActions(publisher, 1);
expect(
screen.queryByRole("button", { name: "Publish source Stockholm" }),
).not.toBeInTheDocument();
expect(
screen.getByRole("button", { name: "Publish visible FASTA" }),
).toBeInTheDocument();
});
});
async function publishVisibleArtifact(label: string): Promise<void> {
await userEvent.click(screen.getByRole("button", { name: label }));
const save = await screen.findByRole("button", { name: "Save" });
await waitFor(() => expect(save).toBeEnabled());
await userEvent.click(save);
}
function annotatedSequenceDocument() {
return parseSequenceDocument({
contents: `LOCUS demo 12 bp DNA circular
ACCESSION demo
FEATURES Location/Qualifiers
misc_feature complement(join(1..3,7..9))
/gene="GENE1"
/transcript_id="TX1"
ORIGIN
1 acgtacgtacgt
//`,
fileName: "demo.gb",
});
}
function renderAlignmentActions(
publisher: SequenceWorkspaceArtifactPublisher | undefined,
sourceRevision: number,
): void {
render(
<IntlProvider locale="en" messages={{}}>
<MsaExportActions
hits={[]}
publishWorkspaceArtifact={publisher}
referenceLabel="reference"
referenceSequence="ACGT"
rows={[
{
alignedSequence: "ACGT",
id: "reference",
label: "reference",
ungappedLength: 4,
},
]}
selectedColumnRange={null}
sourceRevision={sourceRevision}
visibleColumnEnd={4}
visibleColumnStart={1}
/>
</IntlProvider>,
);
}
function stubClipboard(writeText: (value: string) => Promise<void>): void {
vi.stubGlobal(
"navigator",
Object.assign(Object.create(navigator), { clipboard: { writeText } }),
);
}
function createPublisher(name: string) {
const publisher = vi.fn(
async (
_artifact: PreparedSequenceWorkspaceArtifact,
_relativePath: string,
_collisionPolicy?: "exact" | "next-version",
) => workspaceResult(name),
);
return Object.assign(publisher, {
createDirectory: vi.fn(),
listDirectory: vi.fn(async () => ({
candidate: {
exactAvailable: true,
exactWorkspacePath: `exports/${name}`,
name,
nextVersionName: name,
nextVersionWorkspacePath: `exports/${name}`,
},
directory: {
breadcrumbs: [
{ label: "Workspace", relativePath: ".", workspacePath: "." },
],
relativePath: ".",
sourceDirectoryWorkspacePath: ".",
workspacePath: ".",
},
entries: [],
omittedEntries: 0,
})),
}) as typeof publisher & SequenceWorkspaceArtifactPublisher;
}
function workspaceResult(name: string) {
return {
destination: { base: "opened-source" as const, kind: "workspace" as const },
format: "fasta" as const,
kind: "artifact" as const,
mediaType: "text/x-fasta",
name,
outputWorkspacePath: `exports/${name}`,
provenanceWorkspacePath: `exports/${name}.provenance.json`,
sha256: "a".repeat(64),
size: 8,
version: 1 as const,
};
}
SHA-256: 947cb10c7b8d5ee0189990a6c100a4390341af549b7b8bfbeed56acbbdacab4f