Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ validate-all: py-validate
Rscript -e 'renv::restore()'

# === Python Commands for eFP-Seq_Browser ===
.PHONY: py-install py-format py-lint py-test py-clean py-validate
.PHONY: py-install py-format py-lint py-validate

PY_FILES = cgi-bin/*.cgi

Expand All @@ -21,4 +21,4 @@ py-format:
py-lint:
uv run ruff check --fix $(PY_FILES)

py-validate: py-install py-format py-lint
py-validate: py-format py-lint
67 changes: 30 additions & 37 deletions cgi-bin/Submission_page/XMLgenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -329,44 +329,26 @@ function check_links(bam_name, repo_name) {
for (let i = 0; i < x.length; i++) {
if (x[i].id === "bam_input") {
if (x[i].value.length > 0) {
if (bam_x[i].value == "Google Drive") {
// Verify if Google Link
const driveLink = x[i].value.split("//");
if (driveLink.length > 1) {
// If starts with https
const driveURL = driveLink[1];
return (
driveURL.split("/")[0] === "drive.google.com" ||
driveURL.split("/")[0] === "www.drive.google.com"
);
} else if (driveLink.length === 0) {
// If does not start with https
return (
driveLink.split("/")[0] === "drive.google.com" ||
driveLink.split("/")[0] === "www.drive.google.com"
);
} else {
// Not URL
return false;
}
} else if (bam_x[i].value == "Amazon AWS") {
/** Link from string to URL format */
const urlString = new URL(x[i].value);
let urlString;
try {
urlString = new URL(x[i].value);
} catch {
return false;
}

const hostValue = urlString?.host ?? "";

if (bam_x[i].value === "Google Drive") {
return ["drive.google.com", "www.drive.google.com"].includes(hostValue);
} else if (bam_x[i].value === "Amazon AWS") {
if (
(urlString.host("s3.amazonaws.com") || x[i].value.includes("araport.cyverse-cdn.tacc.cloud")) &&
check_amazon_for_bam(x[i].value)
check_amazon_for_bam(x[i].value) &&
(["s3.amazonaws.com", "araport.cyverse-cdn.tacc.cloud"].includes(hostValue) ||
x[i].value.includes("araport.cyverse-cdn.tacc.cloud"))
) {
return true;
} else if (
(urlString.host("s3.amazonaws.com") ||
x[i].value.includes("araport.cyverse-cdn.tacc.cloud/")) &&
check_amazon_for_bam(x[i].value)
) {
return false;
} else {
return false;
}
return false;
} else {
return false;
}
Expand Down Expand Up @@ -507,19 +489,21 @@ function outline_links(bam_name, repo_name) {

/** URL for the hosted BAM file */
let urlValue;
let hostValue = "";

// Convert the
// Convert the string into a URL object if valid
try {
urlValue = new URL(x[i].value);
hostValue = urlValue.host || "";
} catch {
console.error("Unreadable URL presented: ", x[i].value);
}

if (bamHostType && urlValue) {
if (bamHostType && hostValue) {
if (bamHostType === "Amazon AWS") {
if (
check_amazon_for_bam(x[i].value) &&
["s3.amazonaws.com", "araport.cyverse-cdn.tacc.cloud"].includes(urlValue[host])
["s3.amazonaws.com", "araport.cyverse-cdn.tacc.cloud"].includes(hostValue)
) {
x[i].style.borderColor = null;
x[i].style.boxShadow = null;
Expand All @@ -528,7 +512,7 @@ function outline_links(bam_name, repo_name) {
x[i].style.boxShadow = "0 0 10px #ff2626";
}
} else if (bamHostType === "Google Drive") {
if (["drive.google.com"].includes(urlValue[host])) {
if (["drive.google.com", "www.drive.google.com"].includes(hostValue)) {
x[i].style.borderColor = null;
x[i].style.boxShadow = null;
} else {
Expand Down Expand Up @@ -1300,3 +1284,12 @@ function initGen() {
setTimeout(function () {
initGen();
}, 1000);

// Export functions for testing in Node.js environments
if (typeof module !== "undefined" && module.exports) {
module.exports = {
update,
check_links,
check_amazon_for_bam,
};
}
21 changes: 20 additions & 1 deletion cgi-bin/Submission_page/XMLgenerator.min.js

Large diffs are not rendered by default.

213 changes: 143 additions & 70 deletions cgi-bin/Submission_page/XMLgenerator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,59 +2,53 @@
* @jest-environment jsdom
*/

const fs = require("fs");
const path = require("path");
const elementValueMap = {
reqxml: { value: "TestXML" },
reqauthor: { value: "Author" },
contectinfo: { value: "contact@email.com" },
};

describe("update (XMLgenerator.js)", () => {
let update;
let $;
// Lightweight jQuery stub to satisfy update()
global.$ = function () {
const valueMap = {
".channelcontrols": "ctrl1, ctrl2",
".channelgroupwidtho": "rep1, rep2",
".channeldescription": "desc",
".channelrecordnumber": "42",
".channelhexcolor": "#ff0000",
".channelbamType": "Google Drive",
".channelbamlink": "https://drive.google.com/file/d/abc",
".channeltotalreadsmapped": "12345",
".channelreadmapmethod": "STAR",
".channelpublicationlink": "https://pub.com/xyz",
".channeltissue": "Leaf",
".channelsvgname": "ath-leaf.svg",
".channeltitle": "Test Title",
".channelsralink": "SRR000001",
".channelspecies": "Arabidopsis",
".channelforeground": "#000000",
".channelfilename": "file1.bam",
};

beforeAll(() => {
// Minimal jQuery mock
$ = () => ({
find: (selector) => ({
val: () => {
const map = {
".channelcontrols": "ctrl1, ctrl2",
".channelgroupwidtho": "rep1, rep2",
".channeldescription": "desc",
".channelrecordnumber": "42",
".channelhexcolor": "#ff0000",
".channelbamType": "Google Drive",
".channelbamlink": "https://drive.google.com/file/d/abc",
".channeltotalreadsmapped": "12345",
".channelreadmapmethod": "STAR",
".channelpublicationlink": "https://pub.com/xyz",
".channeltissue": "Leaf",
".channelsvgname": "ath-leaf.svg",
".channeltitle": "Test Title",
".channelsralink": "SRR000001",
".channelspecies": "Arabidopsis",
".channelforeground": "#000000",
".channelfilename": "file1.bam",
};
return map[selector] || "dummy";
},
}),
});
return {
find: (childSelector) => ({ val: () => valueMap[childSelector] || "" }),
};
};
global.$.fn = {};

// Robust document.getElementById mock for all ids
const getElementByIdMock = (id) => {
const values = {
reqxml: { value: "TestXML" },
reqauthor: { value: "Author" },
contectinfo: { value: "contact@email.com" },
};
return values[id] || { value: "dummy" };
};
if (global.document) {
global.document.getElementById = getElementByIdMock;
} else {
global.document = { getElementById: getElementByIdMock };
}
global.$ = $;
// Mock document.getElementById before requiring modules
global.document = {
...global.document,
getElementById: (id) => elementValueMap[id] || { value: "" },
};

const xmlGeneratorModule = require("./XMLgenerator.js");

// Required top-level variables for update()
describe("XMLgenerator", () => {
const { update, check_links, check_amazon_for_bam } = xmlGeneratorModule;

beforeEach(() => {
global.document.getElementById = (id) => elementValueMap[id] || { value: "" };
global.topXML = [
'\t\t<file info="<?channeldescription?>" record_number="<?channelrecordnumber?>" foreground="<?channelforeground?>" hex_colour="<?channelhexcolor?>" bam_type="<?channelbamType?>" name="<?channelbamlink?>" filename="<?channelfilename?>" total_reads_mapped="<?channeltotalreadsmapped?>" read_map_method="<?channelreadmapmethod?>" publication_link="<?channelpublicationlink?>" svg_subunit="<?channeltissue?>" svgname="<?channelsvgname?>" description="<?channeltitle?>" url="<?channelsralink?>" species="<?channelspecies?>" title="<?channeligbtitle?>">',
"\t\t\t<controls>\n",
Expand All @@ -65,30 +59,109 @@ describe("update (XMLgenerator.js)", () => {
global.existingXML = "";
global.all_controls = "";
global.all_replicates = "";
});

describe("update", () => {
const cases = [
{
name: "includes all substituted values",
want: [
"desc",
'record_number="42"',
'hex_colour="#ff0000"',
'bam_type="Google Drive"',
'name="https://drive.google.com/file/d/abc"',
'filename="file1.bam"',
"<bam_exp>ctrl1</bam_exp>",
"<bam_exp>rep2</bam_exp>",
'total_reads_mapped="12345"',
'read_map_method="STAR"',
'species="Arabidopsis"',
'svg_subunit="Leaf"',
],
},
];

it.each(cases)("$name", ({ want }) => {
const result = update("", {});
want.forEach((expected) => expect(result).toContain(expected));
expect(result).toContain("</file>");
});
});

// Load the update function from XMLgenerator.js
const code = fs.readFileSync(path.join(__dirname, "../Submission_page/XMLgenerator.js"), "utf8");
const fnMatch = code.match(/function update\s*\(([^)]*)\)\s*{([\s\S]*?)^}/m);
if (!fnMatch) throw new Error("update function not found in XMLgenerator.js");
const args = fnMatch[1];
const body = fnMatch[2];
describe("check_amazon_for_bam", () => {
const cases = [
{ name: "accepts .bam extension", input: "https://s3.amazonaws.com/bucket/file.bam", expected: true },
{ name: "accepts local .bam", input: "file.bam", expected: true },
{ name: "rejects non-bam extension", input: "https://s3.amazonaws.com/bucket/file.txt", expected: false },
{ name: "rejects alternate extension", input: "https://example.com/path/to/file.bed", expected: false },
];

update = new Function(args, body);
it.each(cases)("$name", ({ input, expected }) => {
expect(check_amazon_for_bam(input)).toBe(expected);
});
});

it("generates correct XML for given form values", () => {
const v = {}; // dummy, not used in our $ mock
const result = update("", v);
// Check for key XML elements and values
expect(result).toContain('info="desc"');
expect(result).toContain('record_number="42"');
expect(result).toContain('hex_colour="#ff0000"');
expect(result).toContain('bam_type="Google Drive"');
expect(result).toContain('name="https://drive.google.com/file/d/abc"');
expect(result).toContain('filename="file1.bam"');
expect(result).toContain("<bam_exp>ctrl1</bam_exp>");
expect(result).toContain("<bam_exp>ctrl2</bam_exp>");
expect(result).toContain("<bam_exp>rep1</bam_exp>");
expect(result).toContain("<bam_exp>rep2</bam_exp>");
describe("check_links", () => {
const buildDom = (bamType, bamLink) => {
const bamInput = { id: "bam_input", value: bamLink, style: {} };
const bamTypeNode = { value: bamType };
global.document.getElementById = (id) => {
if (id === "Entries_all") {
return {
querySelectorAll: (selector) => {
if (selector === ".bam_link") return [bamInput];
if (selector === ".channelbamType") return [bamTypeNode];
return [];
},
};
}
return elementValueMap[id] || { value: "" };
};
};

const cases = [
{
name: "valid Amazon S3 bam link",
bamType: "Amazon AWS",
link: "https://s3.amazonaws.com/test/file.bam",
expected: true,
},
{
name: "valid Cyverse bam link",
bamType: "Amazon AWS",
link: "https://araport.cyverse-cdn.tacc.cloud/rnaseq/file.bam",
expected: true,
},
{
name: "Amazon link missing .bam",
bamType: "Amazon AWS",
link: "https://s3.amazonaws.com/test/file.txt",
expected: false,
},
{
name: "valid Google Drive link",
bamType: "Google Drive",
link: "https://drive.google.com/file/d/123456/view",
expected: true,
},
{
name: "invalid Google host",
bamType: "Google Drive",
link: "https://invalid.google.com/file/d/123456/view",
expected: false,
},
{
name: "unknown bam type",
bamType: "Unknown",
link: "https://example.com/file.bam",
expected: false,
},
];

it.each(cases)("$name", ({ bamType, link, expected }) => {
buildDom(bamType, link);
expect(check_links(".channelbamType", ".bam_link")).toBe(expected);
});
});
});
2 changes: 1 addition & 1 deletion cgi-bin/Submission_page/submissionStyle.min.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading