← Files Biological Sequence & Alignment ViewerARCHIVED FILE
src/sequence/preview.test.tsx
67.6 KB · Sep 30, 2026 · 23:01 UTC
import {
cleanup,
fireEvent,
render,
screen,
waitFor,
within,
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { parseSequenceDocument } from "./parser";
import { summarizeQuality } from "./quality";
import { SequenceRichViewer } from "./sequence-rich-viewer";
import * as artifactStateKey from "../artifact-state-key";
import type {
QueuedSequenceViewerCommand,
SequenceViewerCommand,
} from "../viewer-commands";
import { parseWorkbenchSession } from "../workbench-state";
import { createSequenceInterfaceSettings } from "./interface-state";
afterEach(() => cleanup());
describe("SequenceRichViewer", () => {
it("skips content hashing for an authenticated source key and preserves the fallback", () => {
const createArtifactStateKey = vi.spyOn(
artifactStateKey,
"createArtifactStateKey",
);
const document = parseSequenceDocument({
contents: ">trusted\nACGT\n",
fileName: "trusted.fasta",
});
try {
const view = render(
<SequenceRichViewer
document={document}
sourceStateKeyOverride="authenticated-source-revision"
/>,
);
expect(createArtifactStateKey).not.toHaveBeenCalled();
view.rerender(<SequenceRichViewer document={document} />);
expect(createArtifactStateKey).toHaveBeenCalledWith(
"trusted\u001fACGT",
"trusted.fasta",
);
} finally {
createArtifactStateKey.mockRestore();
}
});
it("renders aligned FASTA rows in generic sequence mode with a compact legend and searchable wrapped cells", async () => {
render(
<SequenceRichViewer
document={parseSequenceDocument({
contents: ">a\nAC-GT\n>b\nACTGT\n",
fileName: "family.fasta",
})}
/>,
);
expect(
screen.getByLabelText("Soft nucleotide palette legend"),
).toBeInTheDocument();
expect(screen.getByLabelText("Wrapped sequence view")).toBeInTheDocument();
await userEvent.type(screen.getByLabelText("Search sequence"), "AC");
expect(await screen.findByText(/Search hits \(/)).toBeInTheDocument();
expect(screen.getByLabelText("a position 1 A")).toBeInTheDocument();
});
it("keeps advanced display controls out of the canvas while preserving their state", async () => {
render(
<SequenceRichViewer
document={parseSequenceDocument({
contents: ">display\nACGTACGT\n",
fileName: "display.fasta",
})}
/>,
);
expect(
screen.getByRole("grid", { name: "Wrapped sequence view" }),
).toBeVisible();
expect(
screen.queryByRole("combobox", { name: "Residue palette" }),
).not.toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: "Display" }));
const palette = screen.getByRole("combobox", { name: "Residue palette" });
await userEvent.selectOptions(palette, "nucleotide-ambiguity");
await userEvent.click(screen.getByRole("button", { name: "Display" }));
expect(palette.isConnected).toBe(true);
expect(palette.closest(".bio-workbench-tool-panel")).toHaveAttribute(
"hidden",
);
expect(palette.closest(".bio-workbench-tool-panel")).toHaveAttribute(
"inert",
);
await userEvent.click(screen.getByRole("button", { name: "Display" }));
expect(screen.getByRole("combobox", { name: "Residue palette" })).toBe(
palette,
);
expect(palette).toHaveValue("nucleotide-ambiguity");
});
it("supports submitting a coordinate without opening advanced tools", async () => {
const updateModelContext = vi.fn();
render(
<SequenceRichViewer
document={parseSequenceDocument({
contents: ">jump\nACGTACGT\n",
fileName: "jump.fasta",
})}
updateModelContext={updateModelContext}
/>,
);
await userEvent.type(
screen.getByRole("textbox", { name: "Jump to coordinate" }),
"6{Enter}",
);
await waitFor(() => {
expect(updateModelContext).toHaveBeenLastCalledWith(
expect.objectContaining({
structuredContent: expect.objectContaining({
selection: expect.objectContaining({ start: 6, end: 6 }),
}),
}),
);
});
});
it("uses the same record browser state for user filtering and agent commands", async () => {
const document = parseSequenceDocument({
contents:
">first Alpha\nACGT\n>second Beta\nACGTACGT\n>third Gamma\nACG\n",
fileName: "QA_record_browser.fasta",
});
const onCommandResult = vi.fn();
const viewer = (command?: SequenceViewerCommand, revision = 1) => (
<SequenceRichViewer
command={
command == null ? undefined : sequenceCommand(command, revision)
}
document={document}
onCommandResult={onCommandResult}
/>
);
const view = render(
viewer({
action: "set_sequence_record_browser",
expanded: true,
page: 99,
query: "Beta",
sortBy: "length",
}),
);
await waitFor(() =>
expect(onCommandResult).toHaveBeenCalledWith(
expect.objectContaining({ action: "set_sequence_record_browser" }),
expect.objectContaining({
applied: true,
state: {
recordBrowser: {
expanded: true,
page: 0,
query: "Beta",
sortBy: "length",
},
},
}),
),
);
expect(within(screen.getByRole("table")).getByText("second")).toBeVisible();
expect(
within(screen.getByRole("table")).queryByText("first"),
).not.toBeInTheDocument();
await userEvent.clear(
screen.getByRole("textbox", { name: "Filter records" }),
);
await userEvent.type(
screen.getByRole("textbox", { name: "Filter records" }),
"Alpha",
);
view.rerender(
viewer(
{ action: "query_viewer", request: { target: "sequence-ui-state" } },
2,
),
);
await waitFor(() =>
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.objectContaining({ action: "query_viewer" }),
expect.objectContaining({
state: {
query: expect.objectContaining({
result: expect.objectContaining({
recordBrowser: {
expanded: true,
page: 0,
query: "Alpha",
sortBy: "length",
},
}),
}),
},
}),
),
);
view.rerender(
viewer({ action: "save_session", name: "QA browser session" }, 3),
);
const savedSession = onCommandResult.mock.calls.at(-1)?.[1].state?.session;
if (typeof savedSession !== "string")
throw new Error("Expected a browser session.");
view.rerender(
viewer(
{
action: "set_sequence_record_browser",
query: "Gamma",
expanded: false,
sortBy: "source",
},
4,
),
);
view.rerender(
viewer({ action: "restore_session", session: savedSession }, 5),
);
expect(screen.getByRole("textbox", { name: "Filter records" })).toHaveValue(
"Alpha",
);
expect(within(screen.getByRole("table")).getByText("first")).toBeVisible();
const legacySession = parseWorkbenchSession(savedSession);
if (legacySession.view.sequence == null)
throw new Error("Expected sequence view.");
delete legacySession.view.sequence.interface;
view.rerender(
viewer(
{ action: "restore_session", session: JSON.stringify(legacySession) },
6,
),
);
view.rerender(
viewer(
{ action: "query_viewer", request: { target: "sequence-ui-state" } },
7,
),
);
const defaults = createSequenceInterfaceSettings();
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.objectContaining({ action: "query_viewer" }),
expect.objectContaining({
state: {
query: expect.objectContaining({
result: expect.objectContaining({
recordBrowser: defaults.recordBrowser,
annotationIndex: defaults.annotationIndex,
originRangeExpanded: false,
readPileup: defaults.readPileup,
qualityView: defaults.quality.view,
quality: expect.objectContaining({ adapterSequence: null }),
}),
}),
},
}),
);
});
it("applies trace commands to the visible original-read window and reports user panning", async () => {
const document = parseSequenceDocument({
contents: ">QA_trace\nACGTACGT\n",
fileName: "QA_trace.fasta",
});
const record = document.records[0];
if (record == null) throw new Error("Expected trace fixture.");
record.chromatogram = {
channels: {
A: [8, 0, 0, 0, 8, 0, 0, 0],
C: [0, 8, 0, 0, 0, 8, 0, 0],
G: [0, 0, 8, 0, 0, 0, 8, 0],
T: [0, 0, 0, 8, 0, 0, 0, 8],
},
format: "abif",
peakLocations: [0, 1, 2, 3, 4, 5, 6, 7],
sampleCount: 8,
};
const onCommandResult = vi.fn();
const viewer = (command: SequenceViewerCommand, revision: number) => (
<SequenceRichViewer
command={sequenceCommand(command, revision)}
document={document}
onCommandResult={onCommandResult}
/>
);
const view = render(
viewer(
{
action: "set_chromatogram_view_options",
firstBase: 5,
basesPerWindow: 2,
},
1,
),
);
expect(
await screen.findByRole("figure", {
name: /QA_trace chromatogram, bases 5 to 6/,
}),
).toBeVisible();
await userEvent.click(
screen.getByRole("button", { name: "Next chromatogram window" }),
);
view.rerender(
viewer(
{ action: "query_viewer", request: { target: "sequence-ui-state" } },
2,
),
);
await waitFor(() =>
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.objectContaining({ action: "query_viewer" }),
expect.objectContaining({
state: {
query: expect.objectContaining({
result: expect.objectContaining({
chromatogram: { firstBase: 7, basesPerWindow: 2 },
}),
}),
},
}),
),
);
view.rerender(
viewer({ action: "set_chromatogram_view_options", firstBase: 99 }, 3),
);
await waitFor(() =>
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.objectContaining({ action: "set_chromatogram_view_options" }),
expect.objectContaining({ applied: false }),
),
);
expect(
screen.getByRole("figure", {
name: /QA_trace chromatogram, bases 7 to 8/,
}),
).toBeVisible();
view.rerender(
viewer({ action: "select_sequence_range", start: 1, end: 1 }, 4),
);
view.rerender(
viewer(
{
action: "set_chromatogram_view_options",
firstBase: 7,
basesPerWindow: 2,
},
5,
),
);
view.rerender(
viewer({ action: "save_session", name: "QA trace session" }, 6),
);
const savedSession = onCommandResult.mock.calls.at(-1)?.[1].state?.session;
if (typeof savedSession !== "string")
throw new Error("Expected a serialized trace session.");
expect(
parseWorkbenchSession(savedSession).view.sequence?.selection,
).toMatchObject({ start: 1, end: 1 });
view.rerender(viewer({ action: "clear_sequence_selection" }, 7));
view.rerender(
viewer(
{
action: "set_chromatogram_view_options",
firstBase: 1,
basesPerWindow: 4,
},
8,
),
);
expect(
screen.getByRole("figure", {
name: /QA_trace chromatogram, bases 1 to 4/,
}),
).toBeVisible();
view.rerender(
viewer({ action: "restore_session", session: savedSession }, 9),
);
expect(
screen.getByRole("figure", {
name: /QA_trace chromatogram, bases 7 to 8/,
}),
).toBeVisible();
view.rerender(viewer({ action: "clear_sequence_selection" }, 10));
view.rerender(
viewer({ action: "restore_session", session: savedSession }, 11),
);
expect(
screen.getByRole("figure", {
name: /QA_trace chromatogram, bases 7 to 8/,
}),
).toBeVisible();
});
it("shares read filters and source-read selection between the interface and agent queries", async () => {
const document = parseSequenceDocument({
contents: ">chr1\nACGTACGTACGT\n",
fileName: "QA_read_reference.fasta",
});
const trackId = "22222222-2222-4222-8222-222222222222";
const onCommandResult = vi.fn();
const viewer = (command: SequenceViewerCommand, revision: number) => (
<SequenceRichViewer
command={sequenceCommand(command, revision)}
document={document}
onCommandResult={onCommandResult}
/>
);
const view = render(
viewer(
{
action: "load_track",
content:
"@SQ\tSN:1\tLN:12\nQA-low\t0\t1\t1\t10\t4M\t*\t0\t0\tACGT\tIIII\nQA-high\t0\t1\t5\t60\t4M\t*\t0\t0\tACGT\tIIII\n",
displayName: "QA_reads.sam",
encoding: "utf8",
format: "sam",
reference: "chr1",
trackId,
},
1,
),
);
expect(
await screen.findByRole("button", { name: /Inspect read QA-low,/ }),
).toBeVisible();
await userEvent.click(screen.getByText("Display & filters"));
await userEvent.selectOptions(
screen.getByRole("combobox", { name: "Minimum MAPQ" }),
"20",
);
expect(
screen.queryByRole("button", { name: /Inspect read QA-low,/ }),
).not.toBeInTheDocument();
view.rerender(
viewer(
{
action: "query_viewer",
request: {
target: "reads",
reference: "chr1",
start: 1,
end: 12,
limit: 100,
},
},
2,
),
);
await waitFor(() =>
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.objectContaining({ action: "query_viewer" }),
expect.objectContaining({
state: {
query: expect.objectContaining({
items: [
expect.objectContaining({
id: "QA-high",
trackId,
sourceReadIndex: 1,
}),
],
}),
},
}),
),
);
view.rerender(
viewer({ action: "select_read", trackId, sourceReadIndex: 1 }, 3),
);
expect(
await screen.findByRole("complementary", {
name: "Selected read details",
}),
).toHaveTextContent("QA-high");
view.rerender(
viewer({ action: "save_session", name: "QA read session" }, 4),
);
const savedSession = onCommandResult.mock.calls.at(-1)?.[1].state?.session;
if (typeof savedSession !== "string")
throw new Error("Expected a serialized read session.");
expect(
parseWorkbenchSession(savedSession).view.sequence?.interface?.readPileup,
).toMatchObject({
options: { minimumMappingQuality: 20 },
selectedRead: { trackId, sourceReadIndex: 1 },
});
await userEvent.click(
screen.getByRole("button", { name: "Close read details" }),
);
view.rerender(
viewer(
{ action: "query_viewer", request: { target: "read-pileup-state" } },
5,
),
);
await waitFor(() =>
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.objectContaining({ action: "query_viewer" }),
expect.objectContaining({
state: {
query: expect.objectContaining({
result: expect.objectContaining({
options: expect.objectContaining({ minimumMappingQuality: 20 }),
selectedRead: null,
}),
}),
},
}),
),
);
await userEvent.selectOptions(
screen.getByRole("combobox", { name: "Minimum MAPQ" }),
"0",
);
view.rerender(
viewer({ action: "restore_session", session: savedSession }, 6),
);
expect(
screen.getByRole("complementary", { name: "Selected read details" }),
).toHaveTextContent("QA-high");
expect(screen.getByRole("combobox", { name: "Minimum MAPQ" })).toHaveValue(
"20",
);
const invalidSession = parseWorkbenchSession(savedSession);
const invalidInterface = invalidSession.view.sequence?.interface;
if (invalidInterface == null)
throw new Error("Expected saved interface settings.");
invalidInterface.readPileup.selectedRead = { trackId, sourceReadIndex: 99 };
view.rerender(
viewer(
{ action: "restore_session", session: JSON.stringify(invalidSession) },
7,
),
);
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.objectContaining({ action: "restore_session" }),
expect.objectContaining({ applied: false }),
);
expect(
screen.getByRole("complementary", { name: "Selected read details" }),
).toHaveTextContent("QA-high");
view.rerender(
viewer(
{
action: "edit_copy",
request: { operation: "delete-sequence-range", start: 1, end: 1 },
},
8,
),
);
expect(
screen.queryByRole("complementary", { name: "Selected read details" }),
).not.toBeInTheDocument();
view.rerender(
viewer({ action: "restore_session", session: savedSession }, 9),
);
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.objectContaining({ action: "restore_session" }),
expect.objectContaining({ applied: true }),
);
expect(
screen.getByRole("complementary", { name: "Selected read details" }),
).toHaveTextContent("QA-high");
});
it("returns the displayed QC adapter report after user and model analyses", async () => {
const document = parseSequenceDocument({
contents:
"@QA-read1\nACGTACGTAAAA\n+\nIIIIIIIIIIII\n@QA-read2\nTTTTTTTTGGGG\n+\nIIIIIIIIIIII\n",
fileName: "QA_adapter_parity.fastq",
});
const onCommandResult = vi.fn();
const updateModelContext = vi.fn();
const viewer = (command?: SequenceViewerCommand, revision = 1) => (
<SequenceRichViewer
command={
command == null ? undefined : sequenceCommand(command, revision)
}
document={document}
onCommandResult={onCommandResult}
updateModelContext={updateModelContext}
/>
);
const view = render(viewer());
await userEvent.click(screen.getByRole("button", { name: "Quality" }));
await userEvent.click(screen.getByText("Adapter screen and methods"));
await userEvent.type(
screen.getByRole("textbox", { name: "Your adapter sequence" }),
"ACGTACGT",
);
await userEvent.click(
screen.getByRole("button", { name: "Screen sequence" }),
);
await waitFor(() =>
expect(updateModelContext).toHaveBeenLastCalledWith(
expect.objectContaining({
structuredContent: expect.objectContaining({
interface: expect.objectContaining({
quality: expect.objectContaining({
adapterSequence: "ACGTACGT",
pending: false,
reportAvailable: true,
}),
}),
}),
}),
),
);
view.rerender(
viewer(
{ action: "query_viewer", request: { target: "quality-report" } },
1,
),
);
await waitFor(() =>
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.objectContaining({ action: "query_viewer" }),
expect.objectContaining({
state: {
query: expect.objectContaining({
result: expect.objectContaining({
adapterSequence: "ACGTACGT",
adapter: expect.objectContaining({ records: 1 }),
scope: expect.objectContaining({ analyzedReads: 2 }),
}),
}),
},
}),
),
);
view.rerender(
viewer({ action: "save_session", name: "QA quality session" }, 2),
);
const savedSession = onCommandResult.mock.calls.at(-1)?.[1].state?.session;
if (typeof savedSession !== "string")
throw new Error("Expected a quality session.");
expect(
parseWorkbenchSession(savedSession).view.sequence?.interface?.quality,
).toEqual({
adapterSequence: "ACGTACGT",
view: {
...createSequenceInterfaceSettings().quality.view,
methodsExpanded: true,
},
});
view.rerender(
viewer(
{
action: "run_analysis",
jobId: "33333333-3333-4333-8333-333333333333",
request: { analysis: "quality-report", adapterSequence: "TTTTTTTT" },
},
3,
),
);
await waitFor(() =>
expect(updateModelContext).toHaveBeenLastCalledWith(
expect.objectContaining({
structuredContent: expect.objectContaining({
interface: expect.objectContaining({
quality: expect.objectContaining({
adapterSequence: "TTTTTTTT",
pending: false,
reportAvailable: true,
}),
}),
}),
}),
),
);
const methods = screen
.getByText("Adapter screen and methods")
.closest("details");
if (methods == null)
throw new Error("Expected adapter methods disclosure.");
expect(
within(methods).getByText("TTTTTTTT", { selector: "code" }),
).toBeVisible();
view.rerender(
viewer(
{
action: "set_quality_view_options",
methodsExpanded: false,
distributionsExpanded: true,
expandedTables: [],
},
4,
),
);
view.rerender(
viewer({ action: "restore_session", session: savedSession }, 5),
);
await waitFor(() =>
expect(updateModelContext).toHaveBeenLastCalledWith(
expect.objectContaining({
structuredContent: expect.objectContaining({
interface: expect.objectContaining({
quality: expect.objectContaining({
adapterSequence: "ACGTACGT",
pending: false,
reportAvailable: true,
}),
qualityView: {
...createSequenceInterfaceSettings().quality.view,
methodsExpanded: true,
},
}),
}),
}),
),
);
expect(
within(methods).getByText("ACGTACGT", { selector: "code" }),
).toBeVisible();
view.rerender(
viewer(
{ action: "query_viewer", request: { target: "quality-report" } },
6,
),
);
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.objectContaining({ action: "query_viewer" }),
expect.objectContaining({
state: {
query: expect.objectContaining({
result: expect.objectContaining({
adapterSequence: "ACGTACGT",
adapter: expect.objectContaining({ records: 1 }),
}),
}),
},
}),
);
});
it("does not expose a completed quality report while replacing its source", async () => {
const initial = parseSequenceDocument({
contents: "@QA-old\nACGTACGT\n+\nIIIIIIII\n",
fileName: "QA_old.fastq",
});
const replacement = parseSequenceDocument({
contents: "@QA-new\nAAAA\n+\n5555\n",
fileName: "QA_new.fastq",
});
const onCommandResult = vi.fn();
const updateModelContext = vi.fn();
const view = render(
<SequenceRichViewer
document={initial}
onCommandResult={onCommandResult}
updateModelContext={updateModelContext}
/>,
);
await waitFor(() =>
expect(updateModelContext).toHaveBeenLastCalledWith(
expect.objectContaining({
structuredContent: expect.objectContaining({
interface: expect.objectContaining({
quality: expect.objectContaining({ reportAvailable: true }),
}),
}),
}),
),
);
view.rerender(
<SequenceRichViewer
command={sequenceCommand(
{ action: "query_viewer", request: { target: "quality-report" } },
1,
)}
document={replacement}
onCommandResult={onCommandResult}
updateModelContext={updateModelContext}
/>,
);
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.anything(),
expect.objectContaining({
state: {
query: expect.objectContaining({
result: expect.objectContaining({ reportAvailable: false }),
}),
},
}),
);
await waitFor(() =>
expect(updateModelContext).toHaveBeenLastCalledWith(
expect.objectContaining({
structuredContent: expect.objectContaining({
artifact: expect.objectContaining({ fileName: "QA_new.fastq" }),
interface: expect.objectContaining({
quality: expect.objectContaining({
pending: false,
reportAvailable: true,
}),
}),
}),
}),
),
);
view.rerender(
<SequenceRichViewer
command={sequenceCommand(
{ action: "query_viewer", request: { target: "quality-report" } },
2,
)}
document={replacement}
onCommandResult={onCommandResult}
/>,
);
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.anything(),
expect.objectContaining({
state: {
query: expect.objectContaining({
result: expect.objectContaining({
scope: expect.objectContaining({
analyzedBases: 4,
analyzedReads: 1,
}),
}),
}),
},
}),
);
});
it("recomputes quality after a UI edit while the handled analysis command remains mounted", async () => {
const document = parseSequenceDocument({
contents: "@QA-edit\nACGTACGT\n+\nIIIIIIII\n",
fileName: "QA_edit.fastq",
});
const onCommandResult = vi.fn();
const updateModelContext = vi.fn();
const command = sequenceCommand(
{
action: "run_analysis",
jobId: "33333333-3333-4333-8333-333333333335",
request: { analysis: "quality-report", adapterSequence: "ACGTACGT" },
},
1,
);
const view = render(
<SequenceRichViewer
command={command}
document={document}
onCommandResult={onCommandResult}
updateModelContext={updateModelContext}
/>,
);
await waitFor(() =>
expect(updateModelContext).toHaveBeenLastCalledWith(
expect.objectContaining({
structuredContent: expect.objectContaining({
interface: expect.objectContaining({
quality: expect.objectContaining({
pending: false,
reportAvailable: true,
}),
}),
}),
}),
),
);
const beforeJobId =
updateModelContext.mock.calls.at(-1)?.[0].structuredContent.interface
.quality.jobId;
await userEvent.click(screen.getByLabelText("QA-edit position 1 A"));
await userEvent.click(screen.getByRole("button", { name: "Edit copy" }));
await userEvent.click(
screen.getByRole("button", { name: "Delete selection" }),
);
await waitFor(() =>
expect(updateModelContext).toHaveBeenLastCalledWith(
expect.objectContaining({
structuredContent: expect.objectContaining({
displayedRecord: expect.objectContaining({ length: 7 }),
interface: expect.objectContaining({
quality: expect.objectContaining({
adapterSequence: "ACGTACGT",
pending: false,
reportAvailable: true,
jobId: expect.not.stringMatching(beforeJobId),
}),
}),
}),
}),
),
);
view.rerender(
<SequenceRichViewer
command={sequenceCommand(
{ action: "query_viewer", request: { target: "quality-report" } },
2,
)}
document={document}
onCommandResult={onCommandResult}
/>,
);
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.anything(),
expect.objectContaining({
state: {
query: expect.objectContaining({
result: expect.objectContaining({
adapter: expect.objectContaining({ records: 0 }),
scope: expect.objectContaining({ analyzedBases: 7 }),
}),
}),
},
}),
);
});
it("pushes text plus structured model context for the active record and explicit selection", async () => {
const updateModelContext = vi.fn();
const view = render(
<SequenceRichViewer
document={parseSequenceDocument({
contents: ">demo\nACGTACGT\n",
fileName: "demo.fasta",
})}
updateModelContext={updateModelContext}
/>,
);
const viewQueries = within(view.container);
await waitFor(() => {
expect(updateModelContext).toHaveBeenLastCalledWith(
expect.objectContaining({
structuredContent: expect.objectContaining({ viewer: "sequence" }),
text: expect.stringContaining(
"Current scientific viewer: Sequence viewer",
),
}),
);
});
const contextCallCount = updateModelContext.mock.calls.length;
fireEvent.mouseEnter(viewQueries.getByLabelText("demo position 2 C"));
await waitFor(() => {
expect(updateModelContext).toHaveBeenCalledTimes(contextCallCount);
});
fireEvent.mouseDown(viewQueries.getByLabelText("demo position 2 C"));
fireEvent.mouseUp(viewQueries.getByLabelText("demo position 2 C"));
await waitFor(() => {
expect(updateModelContext).toHaveBeenLastCalledWith(
expect.objectContaining({
structuredContent: expect.objectContaining({
focus: expect.objectContaining({
coordinate: 2,
symbol: "C",
}),
}),
text: expect.stringContaining("Focus: residue 2 C"),
}),
);
});
fireEvent.mouseDown(viewQueries.getByLabelText("demo position 2 C"));
fireEvent.mouseEnter(viewQueries.getByLabelText("demo position 4 T"));
fireEvent.mouseUp(viewQueries.getByLabelText("demo position 4 T"));
await waitFor(() => {
expect(updateModelContext).toHaveBeenLastCalledWith(
expect.objectContaining({
structuredContent: expect.objectContaining({
selection: expect.objectContaining({
end: 4,
residues: [
expect.objectContaining({ coordinate: 2, symbol: "C" }),
expect.objectContaining({ coordinate: 3, symbol: "G" }),
expect.objectContaining({ coordinate: 4, symbol: "T" }),
],
sequence: "CGT",
start: 2,
}),
}),
text: expect.stringContaining("Selection: residues 2-4"),
}),
);
});
expect(updateModelContext).toHaveBeenLastCalledWith(
expect.objectContaining({
text: expect.stringContaining("sequence CGT"),
}),
);
await userEvent.click(viewQueries.getByText("Clear selection"));
await waitFor(() => {
expect(updateModelContext).toHaveBeenLastCalledWith(
expect.objectContaining({
structuredContent: expect.objectContaining({
selection: null,
}),
text: expect.stringContaining("Selection: none"),
}),
);
});
expect(
viewQueries.getByText(/Drag across the sequence/),
).toBeInTheDocument();
});
it("applies focus, range-selection, and motif-search commands from chat", async () => {
const onCommandResult = vi.fn();
const document = parseSequenceDocument({
contents: ">query\nACGTACGT\n>target\nTTTACGTT\n",
fileName: "queries.fasta",
});
const view = render(
<SequenceRichViewer
command={sequenceCommand({
action: "focus_sequence_coordinate",
coordinate: 4,
})}
document={document}
onCommandResult={onCommandResult}
/>,
);
await waitFor(() => {
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.objectContaining({ action: "focus_sequence_coordinate" }),
expect.objectContaining({
applied: true,
state: { coordinate: 4, recordId: "query" },
}),
);
});
view.rerender(
<SequenceRichViewer
command={sequenceCommand(
{
action: "select_sequence_range",
end: 6,
record: "target",
start: 3,
},
2,
)}
document={document}
onCommandResult={onCommandResult}
/>,
);
await waitFor(() => {
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.objectContaining({ action: "select_sequence_range" }),
expect.objectContaining({
applied: true,
state: { end: 6, recordId: "target", start: 3 },
}),
);
});
view.rerender(
<SequenceRichViewer
command={sequenceCommand(
{ action: "search_sequence", query: "ACG", record: "target" },
3,
)}
document={document}
onCommandResult={onCommandResult}
/>,
);
await waitFor(() => {
expect(screen.getByLabelText("Search sequence")).toHaveValue("ACG");
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.objectContaining({ action: "search_sequence" }),
expect.objectContaining({
applied: true,
state: expect.objectContaining({
hitCount: 2,
query: "ACG",
recordId: "target",
}),
}),
);
});
view.rerender(
<SequenceRichViewer
command={sequenceCommand(
{ action: "navigate_sequence_search_hit", direction: "next" },
4,
)}
document={document}
onCommandResult={onCommandResult}
/>,
);
await waitFor(() => {
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.objectContaining({ action: "navigate_sequence_search_hit" }),
expect.objectContaining({
applied: true,
state: expect.objectContaining({ hitCount: 2, hitIndex: 2 }),
}),
);
});
view.rerender(
<SequenceRichViewer
command={sequenceCommand(
{
action: "set_sequence_view_options",
palette: "jalview-nucleotide",
showFeatures: false,
wrapWidth: 80,
},
5,
)}
document={document}
onCommandResult={onCommandResult}
/>,
);
await waitFor(() => {
expect(screen.getByLabelText("Residue palette")).toHaveValue(
"jalview-nucleotide",
);
expect(screen.getByLabelText("Sequence wrap width")).toHaveValue("80");
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.objectContaining({ action: "set_sequence_view_options" }),
expect.objectContaining({
applied: true,
state: expect.objectContaining({
showQuality: false,
showTranslation: false,
}),
}),
);
});
view.rerender(
<SequenceRichViewer
command={sequenceCommand(
{ action: "set_sequence_record", record: "query" },
6,
)}
document={document}
onCommandResult={onCommandResult}
/>,
);
await waitFor(() => {
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.objectContaining({ action: "set_sequence_record" }),
expect.objectContaining({
applied: true,
state: { recordId: "query" },
}),
);
});
view.rerender(
<SequenceRichViewer
command={sequenceCommand({ action: "clear_sequence_selection" }, 7)}
document={document}
onCommandResult={onCommandResult}
/>,
);
await waitFor(() => {
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.objectContaining({ action: "clear_sequence_selection" }),
expect.objectContaining({ applied: true }),
);
});
});
it("keeps generated alignment bytes out of the completed live job result", async () => {
const updateModelContext = vi.fn();
const onOpenAlignment = vi.fn();
const jobId = "22222222-2222-4222-8222-222222222222";
render(
<SequenceRichViewer
command={sequenceCommand({
action: "align_sequences",
jobId,
recordIds: ["a", "b"],
})}
document={parseSequenceDocument({
contents: ">a\nACGT\n>b\nACTT\n",
fileName: "pair.fasta",
})}
onOpenAlignment={onOpenAlignment}
updateModelContext={updateModelContext}
/>,
);
await waitFor(() => expect(onOpenAlignment).toHaveBeenCalledOnce());
await waitFor(() => {
const completedJob = updateModelContext.mock.calls
.map(
([context]) =>
(
context.structuredContent as {
workbench?: {
jobs?: Array<{
id: string;
result?: { artifact?: Record<string, unknown> };
status: string;
}>;
};
}
).workbench?.jobs,
)
.flatMap((jobs) => jobs ?? [])
.find(({ id, status }) => id === jobId && status === "completed");
expect(completedJob?.result?.artifact).toBeDefined();
expect(completedJob?.result?.artifact).not.toHaveProperty("content");
});
});
it("rejects out-of-range coordinates and ambiguous record aliases", async () => {
const onCommandResult = vi.fn();
const document = parseSequenceDocument({
contents: ">alpha shared\nACGT\n>beta shared\nTGCA\n",
fileName: "strict-targets.fasta",
});
const view = render(
<SequenceRichViewer
command={sequenceCommand({
action: "focus_sequence_coordinate",
coordinate: 99,
record: "alpha",
})}
document={document}
onCommandResult={onCommandResult}
/>,
);
await waitFor(() => {
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.anything(),
expect.objectContaining({
applied: false,
message: expect.stringContaining("outside"),
state: { maxCoordinate: 4, minCoordinate: 1 },
}),
);
});
view.rerender(
<SequenceRichViewer
command={sequenceCommand(
{
action: "select_sequence_range",
end: 2,
record: "shared",
start: 1,
},
2,
)}
document={document}
onCommandResult={onCommandResult}
/>,
);
await waitFor(() => {
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.anything(),
expect.objectContaining({
applied: false,
message: expect.stringContaining("More than one sequence record"),
state: { candidateRecordIds: ["alpha", "beta"] },
}),
);
});
});
it("selects an annotated feature from chat by its model-context ID", async () => {
const onCommandResult = vi.fn();
const document = parseSequenceDocument({
contents: `LOCUS DEMO 12 bp DNA linear
DEFINITION demo sequence.
ACCESSION DEMO1
FEATURES Location/Qualifiers
gene 1..12
/gene="foo"
ORIGIN
1 atgaaaacgtgg
//`,
fileName: "demo.gb",
});
const feature = document.records[0]?.features[0];
if (feature == null) {
throw new Error("Expected the GenBank fixture to contain a feature.");
}
render(
<SequenceRichViewer
command={sequenceCommand({
action: "select_sequence_feature",
featureId: feature.id,
})}
document={document}
onCommandResult={onCommandResult}
/>,
);
await waitFor(() => {
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.objectContaining({ action: "select_sequence_feature" }),
expect.objectContaining({
applied: true,
state: expect.objectContaining({ featureId: feature.id }),
}),
);
expect(screen.getByText("Overlapping features")).toBeInTheDocument();
});
});
it("selects a uniquely matched feature by biological type", async () => {
const onCommandResult = vi.fn();
const document = parseSequenceDocument({
contents: `LOCUS DEMO 12 bp DNA linear
DEFINITION demo sequence.
ACCESSION DEMO1
FEATURES Location/Qualifiers
CDS 1..12
/gene="foo"
/translation="MKTW"
ORIGIN
1 atgaaaacgtgg
//`,
fileName: "demo.gb",
});
render(
<SequenceRichViewer
command={sequenceCommand({
action: "select_sequence_feature",
featureId: "cds",
})}
document={document}
onCommandResult={onCommandResult}
/>,
);
await waitFor(() => {
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.objectContaining({ action: "select_sequence_feature" }),
expect.objectContaining({
applied: true,
state: expect.objectContaining({ type: "CDS" }),
}),
);
});
});
it("returns bounded feature candidates when a biological selector is ambiguous", async () => {
const onCommandResult = vi.fn();
const document = parseSequenceDocument({
contents: `LOCUS DEMO 24 bp DNA linear
DEFINITION demo sequence.
ACCESSION DEMO1
FEATURES Location/Qualifiers
CDS 1..12
/gene="foo"
/translation="MKTW"
CDS 13..24
/gene="bar"
/translation="MKTW"
ORIGIN
1 atgaaaacgtggatgaaaacgtgg
//`,
fileName: "demo.gb",
});
render(
<SequenceRichViewer
command={sequenceCommand({
action: "select_sequence_feature",
featureId: "CDS",
})}
document={document}
onCommandResult={onCommandResult}
/>,
);
await waitFor(() => {
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.objectContaining({ action: "select_sequence_feature" }),
expect.objectContaining({
applied: false,
state: expect.objectContaining({
matchingFeatures: [
expect.objectContaining({ id: "CDS-1" }),
expect.objectContaining({ id: "CDS-2" }),
],
matchingFeaturesTruncated: false,
}),
}),
);
});
});
it("renders GenBank feature tracks and FASTQ quality tracks", () => {
const genBankDocument = parseSequenceDocument({
contents: `LOCUS DEMO 12 bp DNA linear
DEFINITION demo sequence.
ACCESSION DEMO1
FEATURES Location/Qualifiers
gene 1..12
/gene="foo"
ORIGIN
1 atgaaaacgtgg
//`,
fileName: "demo.gb",
});
const { rerender } = render(
<SequenceRichViewer document={genBankDocument} />,
);
expect(
screen.getByLabelText("Select gene feature foo"),
).toBeInTheDocument();
rerender(
<SequenceRichViewer
document={parseSequenceDocument({
contents: "@read1\nACGT\n+\nIIII\n",
fileName: "reads.fastq",
})}
/>,
);
expect(screen.getAllByText("FASTQ quality").length).toBeGreaterThan(0);
expect(screen.getByLabelText("Base 1 quality 40")).toBeInTheDocument();
});
it("renders supported long FASTQ reads without overflowing argument limits", async () => {
const length = 150_000;
const document = parseSequenceDocument({
contents: `@long-read\n${"A".repeat(length)}\n+\n${"I".repeat(length - 1)}!\n`,
fileName: "long-reads.fastq",
});
render(
<SequenceRichViewer
document={document}
sourceStateKeyOverride="authenticated-long-read"
/>,
);
await userEvent.click(screen.getByRole("button", { name: "Quality" }));
expect(
screen.getByText(/mean Q40\.0 · min Q0 · max Q40/),
).toBeInTheDocument();
expect(summarizeQuality([])).toEqual({
max: Number.NEGATIVE_INFINITY,
mean: 0,
min: Number.POSITIVE_INFINITY,
});
expect(summarizeQuality([1, Number.NaN, 2])).toEqual({
max: Number.NaN,
mean: Number.NaN,
min: Number.NaN,
});
expect(
summarizeQuality([Number.NEGATIVE_INFINITY, 1, Number.POSITIVE_INFINITY]),
).toEqual({
max: Number.POSITIVE_INFINITY,
mean: Number.NaN,
min: Number.NEGATIVE_INFINITY,
});
});
it("renders aggregate FASTQ QC with explicit Phred+33 provenance", async () => {
render(
<SequenceRichViewer
document={parseSequenceDocument({
contents: [
"@read1",
"ACGN",
"+",
"I5+!",
"@read2",
"GG",
"+",
"??",
].join("\n"),
fileName: "reads.fastq",
})}
/>,
);
await userEvent.click(screen.getByRole("button", { name: "Quality" }));
const panel = screen.getByText("FASTQ overview").closest("section");
expect(panel).not.toBeNull();
if (panel == null) {
throw new Error("Expected the FASTQ overview panel.");
}
const summary = within(panel);
expect(summary.getByText("Read length").parentElement).toHaveTextContent(
"2-4 bp",
);
expect(summary.getByText("GC").parentElement).toHaveTextContent("66.7%");
expect(
summary.getByText("Q20", { selector: "dt" }).parentElement,
).toHaveTextContent("66.7%");
expect(
summary.getByText("Q30", { selector: "dt" }).parentElement,
).toHaveTextContent("50.0%");
expect(summary.getByText(/Phred\+33 assumed/)).toBeInTheDocument();
});
it("renders grouped coordinates, aligned FASTQ quality, hover quality, and drag-pinned ranges", async () => {
const view = render(
<SequenceRichViewer
document={parseSequenceDocument({
contents: "@read1\nACGTACGTACGG\n+\nIIIIIIIIIIII\n",
fileName: "reads.fastq",
})}
/>,
);
const viewQueries = within(view.container);
await userEvent.click(viewQueries.getByRole("button", { name: "Inspect" }));
expect(viewQueries.getAllByText("1").length).toBeGreaterThan(0);
expect(viewQueries.getAllByText("12").length).toBeGreaterThan(0);
expect(
view.container.querySelectorAll("[data-sequence-group]"),
).toHaveLength(2);
expect(
view.container.querySelectorAll("[data-quality-group]"),
).toHaveLength(2);
const positionOne = viewQueries.getByLabelText("read1 position 1 A");
fireEvent.mouseEnter(positionOne);
expect(viewQueries.getByText(/1 · A · Q40/)).toBeInTheDocument();
fireEvent.mouseDown(viewQueries.getByLabelText("read1 position 2 C"));
fireEvent.mouseEnter(viewQueries.getByLabelText("read1 position 4 T"));
fireEvent.mouseUp(viewQueries.getByLabelText("read1 position 4 T"));
expect(viewQueries.getByText("2–4")).toBeInTheDocument();
expect(viewQueries.getByText("CGT")).toBeInTheDocument();
expect(viewQueries.getByText(/FASTQ quality Q40–Q40/)).toBeInTheDocument();
});
it.each([
{
location: "join(1..3,7..9)",
segments: [
{ start: 1, end: 3 },
{ start: 7, end: 9 },
],
},
{
location: "complement(join(1..3,7..9))",
segments: [
{ start: 7, end: 9 },
{ start: 1, end: 3 },
],
},
{
location: "join(OTHER:4..6,1..3,7..9)",
segments: [
{ start: 1, end: 3 },
{ start: 7, end: 9 },
],
},
])(
"preserves local feature segments for map and agent selection: $location",
async ({ location, segments }) => {
const document = parseSequenceDocument({
contents: `LOCUS QA_FEATURE 12 bp DNA linear
DEFINITION Synthetic feature selection geometry.
FEATURES Location/Qualifiers
gene ${location}
/gene="QA feature"
ORIGIN
1 acgtacgtacgt
//`,
fileName: "QA_feature_geometry.gb",
});
const record = document.records[0];
const feature = record?.features[0];
if (record == null || feature == null)
throw new Error("Expected feature fixture.");
const onCommandResult = vi.fn();
const updateModelContext = vi.fn();
const view = render(
<SequenceRichViewer
document={document}
onCommandResult={onCommandResult}
updateModelContext={updateModelContext}
/>,
);
await userEvent.click(
screen.getByRole("button", { name: /^Select QA feature ·/ }),
);
await waitFor(() =>
expect(updateModelContext).toHaveBeenLastCalledWith(
expect.objectContaining({
structuredContent: expect.objectContaining({
selection: expect.objectContaining({
length: 6,
segments,
wraparound: false,
}),
}),
}),
),
);
view.rerender(
<SequenceRichViewer
command={sequenceCommand({
action: "select_sequence_feature",
featureId: feature.id,
})}
document={document}
onCommandResult={onCommandResult}
updateModelContext={updateModelContext}
/>,
);
await waitFor(() =>
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.objectContaining({ action: "select_sequence_feature" }),
expect.objectContaining({
applied: true,
state: expect.objectContaining({
recordId: record.id,
featureId: feature.id,
segments,
}),
}),
),
);
},
);
it("renders strand-aware CDS feature lanes and exact compound codon tracks", async () => {
const reliableDocument = parseSequenceDocument({
contents: `LOCUS DEMO 12 bp DNA linear
DEFINITION demo sequence.
ACCESSION DEMO1
FEATURES Location/Qualifiers
CDS complement(1..12)
/gene="foo"
/translation="MKPG"
ORIGIN
1 atgaaaacgtgg
//`,
fileName: "demo.gb",
});
const view = render(<SequenceRichViewer document={reliableDocument} />);
await userEvent.click(screen.getByRole("button", { name: "Inspect" }));
const featureButton = screen.getByLabelText("Select CDS feature foo");
expect(featureButton).toHaveAttribute("data-feature-strand", "-");
expect(screen.getByText("AA")).toBeInTheDocument();
await userEvent.click(featureButton);
expect(screen.getByText(/aa 1–4/)).toBeInTheDocument();
view.rerender(
<SequenceRichViewer
document={parseSequenceDocument({
contents: `LOCUS DEMO 12 bp DNA linear
DEFINITION demo sequence.
ACCESSION DEMO1
FEATURES Location/Qualifiers
CDS join(1..3,7..9)
/gene="joined"
/translation="MK"
ORIGIN
1 atgaaaacgtgg
//`,
fileName: "joined.gb",
})}
/>,
);
expect(screen.getByText("AA")).toBeInTheDocument();
await userEvent.click(
screen.getByLabelText("Select CDS feature joined segment 1-3"),
);
expect(screen.getByText(/aa 1–1/)).toBeInTheDocument();
expect(screen.getByText(/aa 2–2/)).toBeInTheDocument();
});
it("makes multi-record sequence mode explicit and keeps the record browser available", async () => {
const view = render(
<SequenceRichViewer
document={parseSequenceDocument({
contents: ">short first\nACGT\n>long second\nACGTACGT\n",
fileName: "collection.fasta",
})}
/>,
);
const viewQueries = within(view.container);
expect(viewQueries.getByText("Showing record 1 of 2")).toBeInTheDocument();
expect(
viewQueries.getByText(/starts on the first parsed record from the file/),
).toBeInTheDocument();
await userEvent.clear(viewQueries.getByLabelText("Select sequence record"));
await userEvent.type(
viewQueries.getByLabelText("Select sequence record"),
"2",
);
await userEvent.tab();
expect(viewQueries.getByText("Showing record 2 of 2")).toBeInTheDocument();
expect(viewQueries.getByLabelText("long position 1 A")).toBeInTheDocument();
await userEvent.click(viewQueries.getByText("Browse all records"));
await userEvent.selectOptions(
viewQueries.getByLabelText("Sort records"),
"length",
);
const rows = viewQueries.getAllByRole("row");
expect(rows[1]).toHaveTextContent("long");
await userEvent.type(
viewQueries.getByLabelText("Filter records"),
"second",
);
const recordTable = within(viewQueries.getByRole("table"));
expect(recordTable.queryByText("short")).not.toBeInTheDocument();
await userEvent.click(recordTable.getByText("long"));
expect(viewQueries.getByLabelText("long position 1 A")).toBeInTheDocument();
});
it("shows modality-specific canonical palette options and legends", async () => {
const view = render(
<SequenceRichViewer
document={parseSequenceDocument({
contents: ">dna\nACGTRYN\n",
fileName: "dna.fasta",
})}
/>,
);
const viewQueries = within(view.container);
await userEvent.click(viewQueries.getByRole("button", { name: "Display" }));
expect(viewQueries.getByLabelText("Residue palette")).toHaveValue(
"muted-nucleic-acid",
);
expect(viewQueries.getByLabelText("Residue palette")).toHaveTextContent(
"Soft nucleotide",
);
expect(viewQueries.getByLabelText("Residue palette")).toHaveTextContent(
"Monochrome",
);
expect(viewQueries.getByLabelText("Residue palette")).toHaveTextContent(
"NCBI nucleic acid",
);
expect(viewQueries.getByLabelText("Residue palette")).toHaveTextContent(
"Jalview nucleotide",
);
await userEvent.selectOptions(
viewQueries.getByLabelText("Residue palette"),
"nucleotide-ambiguity",
);
expect(
viewQueries.getByLabelText("Nucleotide ambiguity palette legend"),
).toBeInTheDocument();
expect(viewQueries.getByText("R = A/G")).toBeInTheDocument();
view.rerender(
<SequenceRichViewer
document={parseSequenceDocument({
contents: ">protein\nMKWVTFISLLFLFSSAYS\n",
fileName: "protein.faa",
})}
/>,
);
expect(viewQueries.getByLabelText("Residue palette")).toHaveTextContent(
"RasMol",
);
expect(viewQueries.getByLabelText("Residue palette")).toHaveTextContent(
"ClustalX",
);
expect(viewQueries.getByLabelText("Residue palette")).toHaveTextContent(
"Hydrophobicity",
);
expect(viewQueries.getByLabelText("Residue palette")).toHaveValue(
"muted-amino-acid",
);
expect(viewQueries.getByLabelText("Residue palette")).not.toHaveTextContent(
"Soft nucleotide",
);
expect(viewQueries.getByLabelText("Residue palette")).toHaveTextContent(
"Monochrome",
);
expect(
viewQueries.getByLabelText("Soft amino acid palette legend"),
).toBeVisible();
});
it.each([
{ molecule: "dna", sequence: "ACGTACGT" },
{ molecule: "rna", sequence: "ACGUACGU" },
{ molecule: "nucleic-acid-ambiguous", sequence: "ACGNACGN" },
{ molecule: "protein", sequence: "MKWVTFISLLFLFSSAYS" },
])(
"shares $molecule palette choices between UI and agent commands",
async ({ molecule, sequence }) => {
const defaultPaletteId =
molecule === "protein" ? "muted-amino-acid" : "muted-nucleic-acid";
const defaultPaletteLabel =
molecule === "protein" ? "Soft amino acid" : "Soft nucleotide";
const scientificPalettes =
molecule === "protein"
? ["rasmol", "clustal-x"]
: ["ncbi-nucleic-acid", "jalview-nucleotide"];
const document = parseSequenceDocument({
contents: `>palette\n${sequence}\n`,
fileName: "QA_palette.fasta",
});
expect(document.records[0]?.molecule).toBe(molecule);
const onCommandResult = vi.fn();
const viewer = (command: SequenceViewerCommand, revision: number) => (
<SequenceRichViewer
command={sequenceCommand(command, revision)}
document={document}
onCommandResult={onCommandResult}
/>
);
const queryCommand = {
action: "query_viewer",
request: { target: "sequence-ui-state" },
} as const;
const view = render(viewer(queryCommand, 1));
expect(
onCommandResult.mock.calls.at(-1)?.[1].state?.query?.result.palette,
).toMatchObject({
id: defaultPaletteId,
label: defaultPaletteLabel,
defaultId: defaultPaletteId,
restorationWarning: null,
options: expect.arrayContaining([
expect.objectContaining({
id: defaultPaletteId,
label: defaultPaletteLabel,
}),
expect.objectContaining({ id: "neutral", label: "Monochrome" }),
...scientificPalettes.map((id) => expect.objectContaining({ id })),
]),
});
await userEvent.click(screen.getByRole("button", { name: "Display" }));
const palette = screen.getByRole("combobox", { name: "Residue palette" });
await userEvent.selectOptions(palette, "neutral");
view.rerender(viewer(queryCommand, 2));
expect(
onCommandResult.mock.calls.at(-1)?.[1].state?.query?.result.palette,
).toMatchObject({
id: "neutral",
label: "Monochrome",
});
for (const [index, paletteId] of (
[defaultPaletteId, "neutral"] as const
).entries()) {
view.rerender(
viewer(
{
action: "set_sequence_view_options",
palette: paletteId,
},
index + 3,
),
);
expect(palette).toHaveValue(paletteId);
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.objectContaining({ action: "set_sequence_view_options" }),
expect.objectContaining({
applied: true,
state: expect.objectContaining({ palette: paletteId }),
}),
);
}
expect(screen.getByLabelText("Monochrome palette legend")).toBeVisible();
view.rerender(
viewer(
{
action: "set_sequence_view_options",
palette:
molecule === "protein"
? "muted-nucleic-acid"
: "muted-amino-acid",
},
5,
),
);
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.objectContaining({ action: "set_sequence_view_options" }),
expect.objectContaining({ applied: false }),
);
expect(palette).toHaveValue("neutral");
},
);
it.each(
[
{
molecule: "dna",
sequence: "ACGTACGT",
palettes: [
{ saved: "ncbi-nucleic-acid", expected: "ncbi-nucleic-acid" },
{ saved: "jalview-nucleotide", expected: "jalview-nucleotide" },
{ saved: "muted-nucleic-acid", expected: "muted-nucleic-acid" },
{ saved: "neutral", expected: "neutral" },
{ saved: "retired-palette", expected: "muted-nucleic-acid" },
{ saved: "rasmol", expected: "muted-nucleic-acid" },
],
},
{
molecule: "rna",
sequence: "ACGUACGU",
palettes: [
{ saved: "jalview-nucleotide", expected: "jalview-nucleotide" },
{ saved: "neutral", expected: "neutral" },
{ saved: "retired-palette", expected: "muted-nucleic-acid" },
],
},
{
molecule: "protein",
sequence: "MKWVTFISLLFLFSSAYS",
palettes: [
{ saved: "rasmol", expected: "rasmol" },
{ saved: "clustal-x", expected: "clustal-x" },
{ saved: "zappo", expected: "zappo" },
{ saved: "hydrophobicity", expected: "hydrophobicity" },
{ saved: "muted-amino-acid", expected: "muted-amino-acid" },
{ saved: "neutral", expected: "neutral" },
{ saved: "retired-palette", expected: "muted-amino-acid" },
{ saved: "ncbi-nucleic-acid", expected: "muted-amino-acid" },
],
},
].flatMap(({ molecule, sequence, palettes }) =>
palettes.map((palette) => ({ molecule, sequence, ...palette })),
),
)(
"restores saved $molecule palette $saved through the agent across active-record changes",
async ({ molecule, sequence, saved, expected }) => {
const initialSequence =
molecule === "protein" ? "ACGTACGT" : "MKWVTFISLLFLFSSAYS";
const document = parseSequenceDocument({
contents: `>initial\n${initialSequence}\n>restored\n${sequence}\n`,
fileName: "QA_palette_restore.fasta",
});
const onCommandResult = vi.fn();
const viewer = (command: SequenceViewerCommand, revision: number) => (
<SequenceRichViewer
command={sequenceCommand(command, revision)}
document={document}
onCommandResult={onCommandResult}
/>
);
const view = render(
viewer({ action: "set_sequence_record", record: "restored" }, 1),
);
view.rerender(
viewer({ action: "save_session", name: "QA palette session" }, 2),
);
const session = parseWorkbenchSession(
onCommandResult.mock.calls.at(-1)?.[1].state?.session,
);
if (session.view.sequence == null)
throw new Error("Expected a Sequence session.");
session.view.sequence.paletteId = saved;
view.rerender(
viewer({ action: "set_sequence_record", record: "initial" }, 3),
);
expect(screen.getByLabelText("Residue palette")).toHaveValue(
molecule === "protein" ? "muted-nucleic-acid" : "muted-amino-acid",
);
view.rerender(
viewer(
{ action: "restore_session", session: JSON.stringify(session) },
4,
),
);
expect(onCommandResult).toHaveBeenLastCalledWith(
expect.objectContaining({ action: "restore_session" }),
expect.objectContaining({ applied: true }),
);
expect(screen.getByLabelText("Residue palette")).toHaveValue(expected);
expect(
screen.getByLabelText(`restored position 1 ${sequence[0]}`),
).toBeInTheDocument();
view.rerender(
viewer(
{ action: "query_viewer", request: { target: "sequence-ui-state" } },
5,
),
);
expect(
onCommandResult.mock.calls.at(-1)?.[1].state?.query?.result.palette,
).toMatchObject({
id: expected,
restorationWarning:
saved === expected
? null
: expect.stringContaining(`Saved palette "${saved}"`),
});
if (saved !== expected) {
expect(
screen.getByText(
`Saved palette "${saved}" is unavailable for ${molecule} sequences; using ${molecule === "protein" ? "Soft amino acid" : "Soft nucleotide"} instead.`,
),
).toBeVisible();
}
// Copy edits rematerialize the selected record; they must not reset its display choice.
view.rerender(
viewer(
{
action: "edit_copy",
request: { operation: "delete-sequence-range", start: 1, end: 1 },
},
6,
),
);
expect(
screen.getByLabelText(`restored position 1 ${sequence[1]}`),
).toBeInTheDocument();
expect(screen.getByLabelText("Residue palette")).toHaveValue(expected);
view.rerender(
viewer({ action: "save_session", name: "QA restored palette" }, 7),
);
const resaved = parseWorkbenchSession(
onCommandResult.mock.calls.at(-1)?.[1].state?.session,
);
expect(resaved.schemaVersion).toBe(1);
expect(resaved.view.sequence).toMatchObject({
paletteId: expected,
selectedRecordId: "restored",
});
},
);
it.each([
{
sequence: "ACGTACGT",
saved: "ncbi-nucleic-acid",
expected: "ncbi-nucleic-acid",
},
{
sequence: "ACGTACGT",
saved: "retired-palette",
expected: "muted-nucleic-acid",
},
{
sequence: "ACGUACGU",
saved: "jalview-nucleotide",
expected: "jalview-nucleotide",
},
{ sequence: "MKWVTFISLLFLFSSAYS", saved: "rasmol", expected: "rasmol" },
])(
"restores $saved from a session file using the same compatible palette rules",
async ({ sequence, saved, expected }) => {
const document = parseSequenceDocument({
contents: `>initial\nACGTACGT\n>restored\n${sequence}\n`,
fileName: "QA_palette_file_restore.fasta",
});
const onCommandResult = vi.fn();
const viewer = (command: SequenceViewerCommand, revision: number) => (
<SequenceRichViewer
command={sequenceCommand(command, revision)}
document={document}
onCommandResult={onCommandResult}
/>
);
const view = render(
viewer({ action: "set_sequence_record", record: "restored" }, 1),
);
view.rerender(
viewer({ action: "save_session", name: "QA palette file" }, 2),
);
const session = parseWorkbenchSession(
onCommandResult.mock.calls.at(-1)?.[1].state?.session,
);
if (session.view.sequence == null)
throw new Error("Expected a Sequence session.");
session.view.sequence.paletteId = saved;
view.rerender(
viewer({ action: "set_sequence_record", record: "initial" }, 3),
);
await userEvent.click(screen.getByRole("button", { name: "Export" }));
const content = JSON.stringify(session);
const file = new File([content], "QA_palette_session.json", {
type: "application/json",
});
Object.defineProperty(file, "text", { value: async () => content });
await userEvent.upload(screen.getByLabelText("Restore session"), file);
await waitFor(() =>
expect(screen.getByLabelText("Residue palette")).toHaveValue(expected),
);
expect(
screen.getByLabelText(`restored position 1 ${sequence[0]}`),
).toBeInTheDocument();
view.rerender(
viewer(
{ action: "query_viewer", request: { target: "sequence-ui-state" } },
4,
),
);
expect(
onCommandResult.mock.calls.at(-1)?.[1].state?.query?.result.palette,
).toMatchObject({
id: expected,
restorationWarning:
saved === expected
? null
: expect.stringContaining(`Saved palette "${saved}"`),
});
},
);
it("keeps long wrapped sequence rendering bounded to visible lines", () => {
render(
<SequenceRichViewer
document={parseSequenceDocument({
contents: `>long\n${"ACGT".repeat(2_500)}\n`,
fileName: "long.fasta",
})}
/>,
);
expect(
screen.getAllByRole("gridcell", { name: /long position/i }).length,
).toBeLessThan(1_000);
});
it("uses one roving grid tab stop with arrow-key residue navigation", async () => {
render(
<SequenceRichViewer
document={parseSequenceDocument({
contents: ">keyboard\nACGT\n",
fileName: "keyboard.fasta",
})}
/>,
);
const cells = screen.getAllByRole("gridcell", {
name: /keyboard position/i,
});
expect(cells.filter((cell) => cell.tabIndex === 0)).toHaveLength(1);
cells[0]?.focus();
await userEvent.keyboard("{ArrowRight}");
expect(screen.getByLabelText("keyboard position 2 C")).toHaveFocus();
});
});
function sequenceCommand(
command: SequenceViewerCommand,
revision = 1,
): QueuedSequenceViewerCommand {
return {
...command,
commandId: `11111111-1111-4111-8111-${revision.toString().padStart(12, "0")}`,
revision,
} as QueuedSequenceViewerCommand;
}
SHA-256: b8674126321d52e7d842e65e7dc3f4b94574defc041396e5c0dc95ed564614d0