Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bacpop-181 CSV export project #78

Merged
merged 2 commits into from
Sep 9, 2024
Merged
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
9 changes: 9 additions & 0 deletions app/client-v2/e2e/projectPostRun.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,12 @@ test("can run project multiple times", async ({ page }) => {
await expect(page.getByText("GPSC7")).toBeVisible();
await expect(page.getByText("GPSC4")).toBeVisible();
});

test("can export project data as csv", async ({ page }) => {
uploadFiles(page);

const downloadPromise = page.waitForEvent("download");
await page.getByLabel("Export").click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe(`${projectName}.csv`);
});
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import ProjectFileUpload from "@/components/ProjectView/ProjectFileUpload.vue";
import { MOCK_PROJECT_SAMPLES_BEFORE_RUN } from "@/mocks/mockObjects";
import { MOCK_PROJECT_SAMPLES, MOCK_PROJECT_SAMPLES_BEFORE_RUN } from "@/mocks/mockObjects";
import { useProjectStore } from "@/stores/projectStore";
import { createTestingPinia } from "@pinia/testing";
import userEvent from "@testing-library/user-event";
import { fireEvent, render, screen, waitFor } from "@testing-library/vue";
import PrimeVue from "primevue/config";
import { downloadCsv } from "@/utils/projectCsvUtils";
import type { Mock } from "vitest";

vitest.mock("primevue/usetoast", () => ({
useToast: vitest.fn()
}));
const mockDownloadCsv = downloadCsv as Mock;
vitest.mock("@/utils/projectCsvUtils", () => ({
downloadCsv: vitest.fn()
}));

describe("ProjectFile upload", () => {
it("should render drag and drop section when no files uploaded", () => {
Expand Down Expand Up @@ -187,4 +193,37 @@ describe("ProjectFile upload", () => {

expect(screen.queryByRole("progressbar")).toBeNull();
});

it("should disable export button if not ready to run analysis", async () => {
const testPinia = createTestingPinia();
const store = useProjectStore(testPinia);
// @ts-expect-error: Getter is read only
store.isReadyToRun = false;
store.project.samples = [];
render(ProjectFileUpload, {
global: {
plugins: [testPinia, PrimeVue]
}
});

expect(screen.getByRole("button", { name: /export/i })).toBeDisabled();
});

it("should export when ready to run analysis and clicked", async () => {
const testPinia = createTestingPinia();
const store = useProjectStore(testPinia);
// @ts-expect-error: Getter is read only
store.isReadyToRun = true;
store.project.name = "Test Project";
store.project.samples = MOCK_PROJECT_SAMPLES;
render(ProjectFileUpload, {
global: {
plugins: [testPinia, PrimeVue]
}
});

await userEvent.click(screen.getByRole("button", { name: /export/i }));

expect(mockDownloadCsv).toHaveBeenCalledWith(MOCK_PROJECT_SAMPLES, "Test Project");
});
});
102 changes: 102 additions & 0 deletions app/client-v2/src/__tests__/utils/projectCsvUtils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { AMR, ProjectSample } from "@/types/projectTypes";
import { downloadCsv, convertAmrForCsv, generateCsvContent, triggerCsvDownload } from "@/utils/projectCsvUtils";
import type { Mock } from "vitest";

const mockConvertProbabilityToWord = vitest.fn();
vitest.mock("@/utils/amrDisplayUtils", () => ({
convertProbabilityToWord: () => mockConvertProbabilityToWord()
}));
document.createElement = vitest.fn().mockReturnValue({
click: vitest.fn(),
remove: vitest.fn()
});

describe("projectCsvUtils", () => {
beforeEach(() => {
vitest.clearAllMocks();
});

describe("downloadCsv", () => {
it("should generate and trigger CSV download with correct content and filename", () => {
const samples = [
{
filename: "sample1",
amr: { Penicillin: 0.9, Chloramphenicol: 0.8, Erythromycin: 0.7, Tetracycline: 0.6, Trim_sulfa: 0.5 },
cluster: "cluster1"
}
] as ProjectSample[];
const filename = "test";

downloadCsv(samples, filename);

expect(document.createElement).toHaveBeenCalledWith("a");
const anchor = (document.createElement as Mock).mock.results[0].value;
expect(anchor.href).toContain("data:text/csv;charset=utf-8,");
expect(anchor.download).toBe("test.csv");
expect(anchor.click).toHaveBeenCalled();
expect(anchor.remove).toHaveBeenCalled();
});
});

describe("convertAmrForCsv", () => {
it("should convert AMR object to CSV format", () => {
const amr = {
Penicillin: 0.9,
Chloramphenicol: 0.8,
Erythromycin: 0.7,
Tetracycline: 0.6,
Trim_sulfa: 0.5
} as AMR;

mockConvertProbabilityToWord.mockReturnValue("word");

const result = convertAmrForCsv(amr);

expect(result).toEqual({
Penicillin: "word",
Chloramphenicol: "word",
Erythromycin: "word",
Tetracycline: "word",
Cotrim: "word"
});
});
});

describe("generateCsvContent", () => {
it("should generate CSV content from data array", () => {
const data = [
{ filename: "sample1", Penicillin: "Penicillin-0.9", cluster: "cluster1" },
{ filename: "sample2", Penicillin: "Penicillin-0.8", cluster: "cluster2" }
];

const result = generateCsvContent(data);

expect(result).toBe(
'filename,Penicillin,cluster\n"sample1","Penicillin-0.9","cluster1"\n"sample2","Penicillin-0.8","cluster2"'
);
});

it("should return an empty string for empty data array", () => {
const result = generateCsvContent([]);

expect(result).toBe("");
});
});

describe("triggerCsvDownload", () => {
it("should create an anchor element and trigger download", () => {
const csvContent = "filename,Penicillin,cluster\nsample1,Penicillin-0.9,cluster1";
const filename = "test.csv";

triggerCsvDownload(csvContent, filename);

expect(document.createElement).toHaveBeenCalledWith("a");
const anchor = (document.createElement as Mock).mock.results[0].value;

expect(anchor.href).toBe("data:text/csv;charset=utf-8," + encodeURIComponent(csvContent));
expect(anchor.download).toBe("test.csv");
expect(anchor.click).toHaveBeenCalled();
expect(anchor.remove).toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { useProjectStore } from "@/stores/projectStore";
import { downloadCsv } from "@/utils/projectCsvUtils";

const projectStore = useProjectStore();
defineProps<{
Expand All @@ -22,6 +23,15 @@ defineProps<{
</div>
<div v-else class="flex flex-wrap justify-content-between align-items-center flex-1 gap-2">
<Button label="Upload" outlined icon="pi pi-upload" @click="chooseCallback()" />
<Button label="Run Analysis" outlined @click="runAnalysis" :disabled="!projectStore.isReadyToRun" />
<div class="flex gap-2">
<Button label="Run Analysis" outlined @click="runAnalysis" :disabled="!projectStore.isReadyToRun" />
<Button
icon="pi pi-file-export"
outlined
:disabled="!projectStore.isReadyToRun"
@click="downloadCsv(projectStore.project.samples, projectStore.project.name)"
label="Export"
/>
</div>
</div>
</template>
2 changes: 1 addition & 1 deletion app/client-v2/src/utils/amrDisplayUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type ProbabilityWord =
export type ProbabilityWord =
| "Almost certainly"
| "Highly likely"
| "Very good chance"
Expand Down
38 changes: 38 additions & 0 deletions app/client-v2/src/utils/projectCsvUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { AMR, ProjectSample } from "@/types/projectTypes";
import { convertProbabilityToWord } from "./amrDisplayUtils";

export const downloadCsv = (samples: ProjectSample[], filename: string) => {
const csvData = samples.map((sample) => ({
Filename: sample.filename,
...(sample.amr && convertAmrForCsv(sample.amr)),
Cluster: sample.cluster || ""
}));

const csvContent = generateCsvContent(csvData);
triggerCsvDownload(csvContent, `${filename}.csv`);
};

export const convertAmrForCsv = (amr: AMR) => ({
Penicillin: convertProbabilityToWord(amr.Penicillin, "Penicillin"),
Chloramphenicol: convertProbabilityToWord(amr.Chloramphenicol, "Chloramphenicol"),
Erythromycin: convertProbabilityToWord(amr.Erythromycin, "Erythromycin"),
Tetracycline: convertProbabilityToWord(amr.Tetracycline, "Tetracycline"),
Cotrim: convertProbabilityToWord(amr.Trim_sulfa, "Cotrim")
});

export const generateCsvContent = (data: Record<string, string>[]) => {
if (data.length === 0) return "";

const headers = Object.keys(data[0]);
const rows = data.map((row) => headers.map((header) => `"${row[header]}"`).join(","));
return [headers.join(","), ...rows].join("\n");
};

export const triggerCsvDownload = (csvContent: string, filename: string) => {
const anchor = document.createElement("a");
anchor.href = "data:text/csv;charset=utf-8," + encodeURIComponent(csvContent);
anchor.target = "_blank";
anchor.download = filename;
anchor.click();
anchor.remove();
};
Loading