Skip to content
Open
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
5,551 changes: 3,609 additions & 1,942 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@
"test": "vitest run"
},
"dependencies": {
"@aws-sdk/s3-request-presigner": "^3.855.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.2.0",
"@mui/material": "^7.2.0",
"@mui/x-data-grid": "^8.0.0-beta.3",
"@toolpad/core": "^0.16.0",
"ansi-to-html": "^0.7.2",
"axios": "^1.10.0",
"client-s3": "github:aws-sdk/client-s3",
"next": "15.3.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
Expand Down
78 changes: 78 additions & 0 deletions src/__test__/downloadFile.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { NextApiRequest, NextApiResponse } from "next";
import handler from "../pages/api/downloadFile";

// Use a shared sendMock for all tests
const sendMock = vi.fn();

vi.mock("@aws-sdk/client-s3", () => {
return {
S3Client: vi.fn().mockImplementation(() => ({
send: sendMock
})),
ListObjectsV2Command: vi.fn()
};
});

const S3_BASE_URL = process.env.S3_URL;

describe("GET /api/downloadFile", () => {
let req: Partial<NextApiRequest>;
let res: Partial<NextApiResponse>;

beforeEach(() => {
req = {
query: { workflowId: "test-id" }
};
res = {
status: vi.fn().mockReturnThis(),
json: vi.fn()
};
sendMock.mockReset();
});

it("returns 200 with signed URL and files if workflowId is valid", async () => {
sendMock.mockResolvedValue({
Contents: [
{ Key: "multiqc_report.html" },
{ Key: "file1.txt" },
{ Key: "file2.csv" }
]
});

await handler(req as NextApiRequest, res as NextApiResponse);

expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({
message: "Found HTML file and output files to download",
result: `${S3_BASE_URL}/multiqc_report.html`,
files: [`${S3_BASE_URL}/file1.txt`, `${S3_BASE_URL}/file2.csv`]
});
});

it("returns 404 if no output files found", async () => {
sendMock.mockResolvedValue({ Contents: [{}] });

await handler(req as NextApiRequest, res as NextApiResponse);

expect(res.status).toHaveBeenCalledWith(404);
expect(res.json).toHaveBeenCalledWith({
message: "No output files found!",
result: "",
files: []
});
});

it("returns 500 if S3 throws an error", async () => {
sendMock.mockRejectedValue(new Error("S3 error"));

await handler(req as NextApiRequest, res as NextApiResponse);

expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith({
message: "Internal server error",
result: "",
files: []
});
});
});
77 changes: 77 additions & 0 deletions src/__test__/launchDetails.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import handler from "@/pages/api/launchDetails";
import type { NextApiRequest, NextApiResponse } from "next";

global.fetch = vi.fn(); // Mock fetch globally

const mockRes = () => {
const res: Partial<NextApiResponse> = {};
res.status = vi.fn().mockReturnThis();
res.json = vi.fn().mockReturnThis();
return res as NextApiResponse;
};

describe("GET /api/launchDetails", () => {
beforeEach(() => {
vi.resetAllMocks();
});

it("returns 400 if workflowId is missing", async () => {
const req = {
query: {}
} as Partial<NextApiRequest> as NextApiRequest;

const res = mockRes();

await handler(req, res);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({
message: "Missing workflowId",
workflows: []
});
});

it("returns workflow details when fetch succeeds", async () => {
const mockWorkflow = { id: "wf1", name: "Test Workflow" };

(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
json: vi.fn().mockResolvedValueOnce({ workflow: mockWorkflow }),
status: 200
});

const req = {
query: { workflowId: "wf1" }
} as Partial<NextApiRequest> as NextApiRequest;

const res = mockRes();

await handler(req, res);

expect(fetch).toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({
message: "Got launch details",
workflows: mockWorkflow
});
});

it("returns 500 when fetch throws an error", async () => {
(fetch as ReturnType<typeof vi.fn>).mockRejectedValueOnce(
new Error("Fetch failed")
);

const req = {
query: { workflowId: "wf2" }
} as Partial<NextApiRequest> as NextApiRequest;

const res = mockRes();

await handler(req, res);

expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith({
message: "Internal server error",
workflows: []
});
});
});
82 changes: 82 additions & 0 deletions src/__test__/launchLog.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import handler from "@/pages/api/launchLogs"; // Update the path if needed
import type { NextApiRequest, NextApiResponse } from "next";

global.fetch = vi.fn(); // Mock fetch globally

const mockRes = () => {
const res: Partial<NextApiResponse> = {};
res.status = vi.fn().mockReturnThis();
res.json = vi.fn().mockReturnThis();
return res as NextApiResponse;
};

describe("GET /api/launchLogs", () => {
beforeEach(() => {
vi.resetAllMocks();
});

it("returns 400 if workflowId is missing", async () => {
const req = {
query: {}
} as Partial<NextApiRequest> as NextApiRequest;

const res = mockRes();

await handler(req, res);

expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({
message: "Missing workflowId",
log: []
});
});

it("returns logs when fetch succeeds", async () => {
const mockLog = {
stdout: "log line 1\nlog line 2",
stderr: "",
exitCode: 0
};

(fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
json: vi.fn().mockResolvedValueOnce({ log: mockLog }),
status: 200
});

const req = {
query: { workflowId: "wf-log-test" }
} as Partial<NextApiRequest> as NextApiRequest;

const res = mockRes();

await handler(req, res);

expect(fetch).toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({
message: "Got launch logs",
log: mockLog
});
});

it("returns 500 when fetch fails", async () => {
(fetch as ReturnType<typeof vi.fn>).mockRejectedValueOnce(
new Error("API failure")
);

const req = {
query: { workflowId: "wf-log-error" }
} as Partial<NextApiRequest> as NextApiRequest;

const res = mockRes();

await handler(req, res);

expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith({
message: "Internal server error",
log: []
});
});
});
22 changes: 18 additions & 4 deletions src/components/ParamsAccordion.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ import {
Accordion,
AccordionSummary,
AccordionDetails,
FormControlLabel,
Checkbox,
FormControl,
FormHelperText,
Typography,
Expand All @@ -15,6 +13,7 @@ import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import { InputParams } from "@/models/workflow";
import FTextField from "@/components/form/FTextField";
import DragDropUploader from "./DragDropUploader";
import FCheckbox from "./form/FCheck";

interface ParamAccordionGroupProps {
groupKey: string;
Expand Down Expand Up @@ -48,9 +47,9 @@ export default function ParamAccordionGroup({
switch (param.type) {
case "boolean":
return (
<FormControlLabel
<FCheckbox
key={param.key}
control={<Checkbox name={param.key} />}
name={param.key}
label={param.key}
sx={{ mb: 1 }}
/>
Expand All @@ -64,6 +63,8 @@ export default function ParamAccordionGroup({
label={
param.key.charAt(0).toUpperCase() + param.key.slice(1)
}
required={param.required}
defaultValue={param.enum[0]}
select
helperText={param.help_text}
size="small"
Expand Down Expand Up @@ -93,6 +94,19 @@ export default function ParamAccordionGroup({
key={param.key}
name={param.key}
label={param.key}
defaultValue={param.default}
required={param.required}
rule={{
required: param.required
? `${param.key} is required`
: false,
pattern: param.pattern
? {
value: new RegExp(param.pattern),
message: `${param.key} does not match required pattern`
}
: undefined
}}
helperText={param.description}
sx={{ mb: 2 }}
size="small"
Expand Down
Loading