← Files Biological Sequence & Alignment ViewerARCHIVED FILE
src/sequence/use-sequence-workbench-commands.test.ts
10.2 KB · Sep 30, 2026 · 23:01 UTC
import { act, renderHook } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { SequenceViewerCommandResult } from "../viewer-commands";
import { createSequenceWorkbenchState } from "../workbench-state";
import { parseSequenceDocument } from "./parser";
import type { FastqQualityReportState } from "./fastq-quality-analysis";
import {
runSequenceWorkbenchJob,
useSequenceWorkbenchCommands,
} from "./use-sequence-workbench-commands";
describe("scheduled sequence workbench jobs", () => {
afterEach(() => vi.useRealTimers());
it("fails a queued result when the source copy changes before execution", () => {
vi.useFakeTimers();
const document = parseSequenceDocument({
contents: ">record\nACGTACGT\n",
fileName: "record.fasta",
});
const state = createSequenceWorkbenchState(document);
const dispatch = vi.fn();
runSequenceWorkbenchJob({
analysis: { analysis: "statistics" },
dispatch,
isSourceCurrent: () => false,
selectedRecordId: document.records[0]?.id ?? "",
state,
viewerGeneticCodeId: 1,
});
vi.runAllTimers();
expect(dispatch).toHaveBeenCalledWith(
expect.objectContaining({ type: "start-job" }),
);
expect(dispatch).toHaveBeenLastCalledWith(
expect.objectContaining({
error: expect.stringContaining("source sequence changed"),
type: "fail-job",
}),
);
});
it("runs identical bounded quality reports for UI and model operations", async () => {
vi.useFakeTimers();
const document = parseSequenceDocument({
contents:
"@synthetic-qc-1\nACGTACGT\n+\nIIIIIIII\n@synthetic-qc-2\nACGTACGT\n+\n55555555\n",
fileName: "synthetic-quality-parity.fastq",
});
const state = createSequenceWorkbenchState(document);
const selectedRecordId = document.records[0]!.id;
const uiDispatch = vi.fn();
const modelDispatch = vi.fn();
const uiResult = vi.fn<(result: FastqQualityReportState) => void>();
const modelResult = vi.fn<(result: FastqQualityReportState) => void>();
runSequenceWorkbenchJob({
analysis: { analysis: "quality-report", adapterSequence: "ACGTACGT" },
dispatch: uiDispatch,
onQualityReportChange: uiResult,
selectedRecordId,
state,
viewerGeneticCodeId: 1,
});
renderHook(() =>
useSequenceWorkbenchCommands({
cancelledJobsRef: { current: new Set<string>() },
command: {
action: "run_analysis",
commandId: "00000000-0000-4000-8000-000000000010",
jobId: "00000000-0000-4000-8000-000000000011",
request: { analysis: "quality-report", adapterSequence: "ACGTACGT" },
revision: 1,
},
dispatch: modelDispatch,
handledCommandIdRef: { current: undefined },
hits: [],
onCommandResult: vi.fn(),
onQualityReportChange: modelResult,
onRestoreView: vi.fn(),
selectedRecordId,
sourceStateKey: "synthetic-quality-parity",
state,
view: {
geneticCodeId: 1,
layout: "linear",
orientation: "forward",
paletteId: "neutral",
selectedFeatureId: null,
selectedRecordId,
selection: null,
showFeatures: true,
showQuality: true,
showTranslation: false,
synchronizedViews: true,
viewport: null,
wrapWidth: 60,
},
}),
);
await act(async () => {
await vi.runAllTimersAsync();
});
const uiReport = uiResult.mock.calls.at(-1)?.[0].report;
const modelReport = modelResult.mock.calls.at(-1)?.[0].report;
expect(uiReport).toMatchObject({
adapter: { records: 2 },
analysis: "quality-report",
scope: { analyzedReads: 2, isSubset: false, populationReads: 2 },
});
expect(modelReport).toEqual(uiReport);
for (const dispatch of [uiDispatch, modelDispatch]) {
expect(dispatch).toHaveBeenLastCalledWith(
expect.objectContaining({ result: uiReport, type: "complete-job" }),
);
}
});
it("does not publish cancelled QC jobs and settles the controlled UI state", async () => {
vi.useFakeTimers();
const document = parseSequenceDocument({
contents: "@synthetic-read\nACGT\n+\nIIII\n",
fileName: "cancel.fastq",
});
const state = createSequenceWorkbenchState(document);
const dispatch = vi.fn();
const onQualityReportChange =
vi.fn<(result: FastqQualityReportState) => void>();
const cancelledJobsRef = { current: new Set<string>() };
const id = runSequenceWorkbenchJob({
analysis: { analysis: "quality-report" },
cancelledJobsRef,
dispatch,
onQualityReportChange,
selectedRecordId: document.records[0]!.id,
state,
viewerGeneticCodeId: 1,
});
cancelledJobsRef.current.add(id);
await vi.runAllTimersAsync();
expect(onQualityReportChange).toHaveBeenLastCalledWith({
adapterSequence: null,
error: "Quality report cancelled.",
jobId: id,
pending: false,
report: null,
});
expect(dispatch).not.toHaveBeenCalledWith(
expect.objectContaining({ type: "complete-job" }),
);
});
it("does not let a stale QC completion replace a new source’s controlled report", async () => {
vi.useFakeTimers();
const document = parseSequenceDocument({
contents: "@synthetic-read\nACGT\n+\nIIII\n",
fileName: "stale.fastq",
});
const dispatch = vi.fn();
const onQualityReportChange =
vi.fn<(result: FastqQualityReportState) => void>();
let current = true;
runSequenceWorkbenchJob({
analysis: { analysis: "quality-report" },
dispatch,
isSourceCurrent: () => current,
onQualityReportChange,
selectedRecordId: document.records[0]!.id,
state: createSequenceWorkbenchState(document),
viewerGeneticCodeId: 1,
});
current = false;
await vi.runAllTimersAsync();
expect(onQualityReportChange).toHaveBeenCalledTimes(1);
expect(onQualityReportChange).toHaveBeenCalledWith(
expect.objectContaining({ pending: true, report: null }),
);
expect(dispatch).toHaveBeenLastCalledWith(
expect.objectContaining({
error: expect.stringContaining("source sequence changed"),
type: "fail-job",
}),
);
});
});
describe("persisted Sequence artifact visibility", () => {
it("waits for authenticated persistence before displaying an artifact", async () => {
let resolvePersistence:
((result: SequenceViewerCommandResult) => void) | undefined;
const persistence = new Promise<SequenceViewerCommandResult>((resolve) => {
resolvePersistence = resolve;
});
const onCommandResult = vi.fn(() => persistence);
const dispatch = renderSequenceArtifactExport(onCommandResult);
expect(onCommandResult).toHaveBeenCalledWith(
expect.objectContaining({ action: "export_artifact" }),
expect.objectContaining({ applied: true }),
);
expect(dispatch).not.toHaveBeenCalled();
await act(async () => {
resolvePersistence?.({ applied: true, message: "Persisted artifact." });
await persistence;
});
expect(dispatch).toHaveBeenCalledWith(
expect.objectContaining({
artifact: expect.objectContaining({ format: "fasta" }),
type: "add-artifact",
}),
);
});
it("never displays an artifact when authenticated persistence fails", async () => {
let resolvePersistence:
((result: SequenceViewerCommandResult) => void) | undefined;
const persistence = new Promise<SequenceViewerCommandResult>((resolve) => {
resolvePersistence = resolve;
});
const dispatch = renderSequenceArtifactExport(() => persistence);
await act(async () => {
resolvePersistence?.({
applied: false,
message: "Persistence rejected.",
});
await persistence;
});
expect(dispatch).not.toHaveBeenCalled();
});
it("never displays an artifact when the persistence acknowledgment rejects", async () => {
let rejectPersistence: ((reason: Error) => void) | undefined;
const persistence = new Promise<SequenceViewerCommandResult>(
(_resolve, reject) => {
rejectPersistence = reject;
},
);
const dispatch = renderSequenceArtifactExport(() => persistence);
await act(async () => {
rejectPersistence?.(
new Error("The authenticated viewer session expired."),
);
await persistence.catch(() => undefined);
});
expect(dispatch).not.toHaveBeenCalled();
});
it("preserves immediate artifact visibility for legacy direct UI callbacks", () => {
const dispatch = renderSequenceArtifactExport(() => undefined);
expect(dispatch).toHaveBeenCalledWith(
expect.objectContaining({ type: "add-artifact" }),
);
});
});
function renderSequenceArtifactExport(
onCommandResult: NonNullable<
Parameters<typeof useSequenceWorkbenchCommands>[0]["onCommandResult"]
>,
) {
const document = parseSequenceDocument({
contents: ">record\nACGTACGT\n",
fileName: "record.fasta",
});
const state = createSequenceWorkbenchState(document);
const selectedRecordId = document.records[0]?.id;
if (selectedRecordId == null) {
throw new Error("Expected a parsed Sequence record.");
}
const dispatch = vi.fn();
renderHook(() =>
useSequenceWorkbenchCommands({
cancelledJobsRef: { current: new Set<string>() },
command: {
action: "export_artifact",
commandId: "00000000-0000-4000-8000-000000000001",
destination: { kind: "private" },
format: "fasta",
revision: 1,
scope: "all",
},
dispatch,
handledCommandIdRef: { current: undefined },
hits: [],
onCommandResult,
onRestoreView: vi.fn(),
selectedRecordId,
sourceStateKey: "public-sequence-source",
state,
view: {
geneticCodeId: 1,
layout: "linear",
orientation: "forward",
paletteId: "neutral",
selectedFeatureId: null,
selectedRecordId,
selection: null,
showFeatures: true,
showQuality: true,
showTranslation: true,
synchronizedViews: true,
viewport: null,
wrapWidth: 60,
},
}),
);
return dispatch;
}
SHA-256: 93d9aec8c89181cb2d2995e315cff2cd421c2163ef9a7d976a73c341cf901c0f