Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/bamjs/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BAM Viewer</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
46 changes: 46 additions & 0 deletions packages/bamjs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "@galaxyproject/bamjs",
"version": "0.0.1",
"type": "module",
"author": "Galaxy Project",
"license": "MIT",
"files": [
"static/dist/index.js",
"static/dist/index.css"
],
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"prettier": "prettier --write 'package.json' '*.js' 'src/**/*.js'",
"test": "vitest",
"test:watch": "vitest --watch",
"test:ui": "vitest --ui"
},
"devDependencies": {
"@types/node": "^22.9.0",
"@vitest/ui": "^3.1.4",
"jsdom": "^26.1.0",
"prettier": "^3.3.3",
"typescript": "^5.5.4",
"vite": "^6.3.5",
"vitest": "^3.1.4"
},
"dependencies": {
"@gmod/bam": "^5.0.0",
"axios": "^1.7.7",
"generic-filehandle2": "^2.0.2",
"pako": "^2.1.0"
},
"repository": {
"type": "git",
"url": "git+https://github.com/galaxyproject/galaxy-visualizations.git"
},
"keywords": [
"bam",
"galaxy",
"visualization",
"vanilla-js",
"vite"
]
}
12 changes: 12 additions & 0 deletions packages/bamjs/prettier.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default {
printWidth: 120,
tabWidth: 4,
useTabs: false,
semi: true,
singleQuote: false,
quoteProps: "as-needed",
trailingComma: "es5",
bracketSpacing: true,
arrowParens: "always",
endOfLine: "lf",
};
43 changes: 43 additions & 0 deletions packages/bamjs/public/bamjs.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE visualization SYSTEM "../../visualization.dtd">
<visualization name="BAM Viewer">
<description>A BAM file viewer that displays a plaintext representation of BAM records using bam-js library.</description>
<data_sources>
<data_source>
<model_class>HistoryDatasetAssociation</model_class>
<test test_attr="ext" result_type="datatype">bam</test>
<to_param param_attr="id">dataset_id</to_param>
</data_source>
</data_sources>
<params>
<param type="dataset" var_name_in_template="hda" required="true">dataset_id</param>
</params>
<entry_point entry_point_type="script" src="dist/index.js" />
<settings>
<input>
<label>Maximum Records to Display</label>
<name>max_records</name>
<help>Maximum number of BAM records to display (default: 100)</help>
<type>integer</type>
<value>100</value>
<min>1</min>
<max>1000</max>
</input>
<input>
<label>Region Start Position</label>
<name>region_start</name>
<help>Start position for region to display (0-based, default: 0)</help>
<type>integer</type>
<value>0</value>
<min>0</min>
</input>
<input>
<label>Region End Position</label>
<name>region_end</name>
<help>End position for region to display (default: 10000)</help>
<type>integer</type>
<value>10000</value>
<min>1</min>
</input>
</settings>
</visualization>
150 changes: 150 additions & 0 deletions packages/bamjs/src/BamViewer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";

// Mock the @gmod/bam module to avoid ES module issues in tests
vi.mock("@gmod/bam", () => ({
BamFile: vi.fn(() => ({
getHeader: vi.fn().mockResolvedValue({
version: "1.6",
sortOrder: "coordinate",
references: [
{ name: "chr7", length: 159138663 },
{ name: "chrM", length: 16569 },
],
readGroups: [],
programs: [],
}),
getRecordsForRange: vi.fn().mockResolvedValue([
{
name: "test_read_1",
refName: "chr7",
start: 1000,
end: 1050,
cigar: "50M",
seq: "ATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATCGATC",
qual: "JJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJJ",
flags: 99,
mapq: 60,
tags: { AS: 50, XS: 10, NM: 0, MD: "50" },
},
]),
})),
}));

// Import our BamViewer class
async function importBamViewer() {
const module = await import("./main.js");
return module.BamViewer || window.BamViewer;
}

describe("BamViewer", () => {
let container;
let BamViewer;

beforeEach(async () => {
// Create a container element for testing
container = document.createElement("div");
document.body.appendChild(container);

// Import BamViewer class
BamViewer = await importBamViewer();
});

afterEach(() => {
// Clean up
if (container && container.parentNode) {
container.parentNode.removeChild(container);
}
});

it("renders properly with mock data", async () => {
const viewer = new BamViewer(container, {
datasetUrl: "http://example.com/test.bam",
settings: {
max_records: 100,
region_start: 0,
region_end: 10000,
},
});

// Wait for the viewer to finish loading
await new Promise((resolve) => setTimeout(resolve, 50));

expect(container.textContent).toContain("BAM Header Information");
});

it("displays loading message initially", () => {
// Create viewer but don't wait for async operations
const viewer = new BamViewer(container, {
datasetUrl: "http://example.com/test.bam", // Use HTTP URL to trigger async loading
});

// Should show loading initially since HTTP requests are async
expect(container.textContent).toContain("Loading BAM file");
});

it("handles missing settings gracefully", async () => {
const viewer = new BamViewer(container, {
datasetUrl: "http://example.com/test.bam",
});

// Wait for the viewer to finish loading
await new Promise((resolve) => setTimeout(resolve, 50));

expect(container.textContent).toContain("BAM Header Information");
});

it("formats records correctly", async () => {
const viewer = new BamViewer(container, {
datasetUrl: "http://example.com/test.bam",
});

// Test formatRecord method directly
const mockRecord = {
name: "test_read",
refName: "chr1",
start: 1000,
end: 1050,
cigar: "50M",
seq: "ATCG",
qual: "JJJJ",
flags: 99,
mapq: 60,
tags: { AS: 50 },
};

const formatted = viewer.formatRecord(mockRecord);

expect(formatted.name).toBe("test_read");
expect(formatted.refName).toBe("chr1");
expect(formatted.start).toBe(1000);
expect(formatted.end).toBe(1050);
expect(formatted.flags).toContain("paired");
expect(formatted.flags).toContain("proper_pair");
});

it("formats header correctly", async () => {
const viewer = new BamViewer(container, {
datasetUrl: "http://example.com/test.bam",
});

const mockHeader = {
version: "1.6",
sortOrder: "coordinate",
references: [
{ name: "chr1", length: 248956422 },
{ name: "chr2", length: 242193529 },
],
readGroups: [{ id: "HG001", sample: "NA12878" }],
programs: [{ id: "bwa", name: "bwa", version: "0.7.17" }],
};

const formatted = viewer.formatHeader(mockHeader);

expect(formatted).toContain("BAM Header Information");
expect(formatted).toContain("Version: 1.6");
expect(formatted).toContain("Sort Order: coordinate");
expect(formatted).toContain("chr1 (length: 248956422)");
expect(formatted).toContain("ID: HG001");
expect(formatted).toContain("ID: bwa");
});
});
Loading