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-194 All Beebop changes for multi-db #81

Merged
merged 13 commits into from
Oct 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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,8 @@ npx playwright test
```
from `app/client-v2/`.
To close all components once ready, run `./scripts/stop_test` from root.

### Adding new species

1. Add new database to [mrcdata](https://mrcdata.dide.ic.ac.uk/beebop).
2. Add new species to `args.json` in *beebop_py*
9 changes: 2 additions & 7 deletions app/client-v2/e2e/home.spec.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
import { test, expect, Page } from "@playwright/test";
import { randomProjectName } from "./utils.js";
import { createProject, randomProjectName } from "./utils.js";

test.beforeEach(async ({ page }) => {
await page.goto("");
});

const addProjectNavigateHome = async (page: Page, name: string) => {
await page.getByLabel("Create project").click();
await page.getByLabel("Create", { exact: true }).click();
await page.getByLabel("Select a Species").click();
await page.getByLabel("Streptococcus pneumoniae").click();
await page.getByLabel("Name").fill(name);
await page.getByLabel("Create", { exact: true }).click();
await createProject(page, name)

await expect(page.getByText(name)).toBeVisible();

Expand Down
14 changes: 2 additions & 12 deletions app/client-v2/e2e/projectPostRun.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { expect, test } from "@playwright/test";
import { randomProjectName, uploadFiles } from "./utils.js";
import { createProject, randomProjectName, uploadFiles } from "./utils.js";

let projectName: string;
test.beforeEach(async ({ page }) => {
await page.goto("");
projectName = randomProjectName();
await page.getByPlaceholder("Create new Project").fill(projectName);
await page.getByPlaceholder("Create new Project").press("Enter");
await createProject(page, projectName);
});

test("can run project and view results", async ({ page }) => {
Expand Down Expand Up @@ -89,12 +88,3 @@ 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`);
});
16 changes: 13 additions & 3 deletions app/client-v2/e2e/projectPreRun.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { expect, test } from "@playwright/test";
import { randomProjectName, uploadFiles } from "./utils.js";
import { createProject, randomProjectName, uploadFiles } from "./utils.js";

let projectName: string;
test.beforeEach(async ({ page }) => {
await page.goto("");
await page.getByPlaceholder("Create new Project").fill(randomProjectName());
await page.getByPlaceholder("Create new Project").press("Enter");
projectName = randomProjectName();
await createProject(page, projectName);
});

test("upload multiple files and display amr information", async ({ page }) => {
Expand Down Expand Up @@ -36,3 +37,12 @@ test("shows progress bar whilst uploading files & gone after full uploaded", asy

await expect(page.getByRole("progressbar")).not.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`);
});
9 changes: 9 additions & 0 deletions app/client-v2/e2e/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,12 @@ export const uploadFiles = async (page: Page, files = ["e2e/fastaFiles/good_1.fa
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(files);
};

export const createProject = async (page: Page, name: string, species = "Streptococcus pneumoniae") => {
await page.getByLabel("Create project").click();
await page.getByLabel("Create", { exact: true }).click();
await page.getByLabel("Select a Species").click();
await page.getByLabel(species).click();
await page.getByLabel("Name").fill(name);
await page.getByLabel("Create", { exact: true }).click();
};
16 changes: 10 additions & 6 deletions app/client-v2/public/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,30 @@ function createFS(module, f) {
module.FS.mount(module.FS.filesystems.WORKERFS, { files: [f] }, workdir);
}

async function computeSample(hash, file, filename) {
async function computeSample(fileObject, sketchKmerArguments) {
const { file, hash, filename } = fileObject;
const { kmerMin, kmerMax, kmerStep } = sketchKmerArguments;

const amrModule = await AMRprediction();
const sketchModule = await WebSketch();
//create working directory and mount file
createFS(amrModule, file);
createFS(sketchModule, file);

const amr = amrModule.make_prediction_json(workdir + "/" + filename);
// sketch() takes the followings arguments: filepath, kmer_min, kmer_max, kmer_step,
// bbits, sketchsize64, codon_phased (boolean), use_rc (boolean)
const sketch = sketchModule.sketch(workdir + "/" + filename, 14, 29, 3, 14, 156, false, true);
// sketch() takes the followings arguments:
// filepath, kmer_min, kmer_max, kmer_step, bbits, sketchsize64, codon_phased (boolean), use_rc (boolean)
const sketch = sketchModule.sketch(workdir + "/" + filename, kmerMin, kmerMax, kmerStep, 14, 156, false, true);

return { hash, amr, sketch };
}

onmessage = async function (message) {
const hashedFiles = message.data;
const { hashedFiles, sketchKmerArguments } = message.data;

const samples = [];
for (const fileObject of hashedFiles) {
const { hash, amr, sketch } = await computeSample(fileObject.hash, fileObject.file, fileObject.filename);
const { hash, amr, sketch } = await computeSample(fileObject, sketchKmerArguments);
samples.push({ hash, amr: JSON.parse(amr), sketch: JSON.parse(sketch), filename: fileObject.filename });
}
postMessage(samples);
Expand Down
3 changes: 3 additions & 0 deletions app/client-v2/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
<script setup lang="ts">
import AppNav from "@/layouts/AppNav.vue";
import { useTheme } from "@/composables/useTheme";
import { useSpeciesStore } from "./stores/speciesStore";

const { setInitialTheme } = useTheme();
const { setSpeciesConfig } = useSpeciesStore();
setInitialTheme();
setSpeciesConfig();
</script>

<template>
Expand Down
13 changes: 11 additions & 2 deletions app/client-v2/src/__tests__/App.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import AppVue from "@/App.vue";
import { useSpeciesStore } from "@/stores/speciesStore";
import { createTestingPinia } from "@pinia/testing";
import { render, screen } from "@testing-library/vue";
import { defineComponent } from "vue";
import { createRouter, createWebHistory } from "vue-router";
import PrimeVue from "primevue/config";

Check warning on line 7 in app/client-v2/src/__tests__/App.spec.ts

View workflow job for this annotation

GitHub Actions / lint_frontend (20.x)

'PrimeVue' is defined but never used

const mockedThemeValues = {
setInitialTheme: vitest.fn(),
Expand All @@ -11,25 +14,31 @@
vitest.mock("@/composables/useTheme", () => ({
useTheme: () => mockedThemeValues
}));
vitest.mock("primevue/usetoast", () => ({
useToast: vitest.fn()
}));

const router = createRouter({
history: createWebHistory(),
routes: [{ path: "/about", component: defineComponent({ template: `<div>About Page</div>` }) }]
});
describe("App", () => {
it("should render about page(route) and call setInitialTheme on load of about page", async () => {
it("should render about page(route) and call setInitialTheme & setSpeciesConfig on load of about page", async () => {
router.push("/about");
await router.isReady();

render(AppVue, {
global: {
plugins: [router],
plugins: [router, createTestingPinia()],
stubs: {
AppNav: true
}
}
});

const speciesStore = useSpeciesStore();
expect(screen.getByText(/about page/i)).toBeVisible();
expect(mockedThemeValues.setInitialTheme).toHaveBeenCalled();
expect(speciesStore.setSpeciesConfig).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { createRouter, createWebHistory } from "vue-router";
import { defineComponent } from "vue";
import PrimeVue from "primevue/config";
import ToastService from "primevue/toastservice";
import { MOCK_PROJECTS } from "@/mocks/mockObjects";
import { MOCK_PROJECTS, MOCK_SPECIES } from "@/mocks/mockObjects";
import userEvent from "@testing-library/user-event";
import { server } from "@/mocks/server";
import { http, HttpResponse } from "msw";
import { projectIndexUri } from "@/mocks/handlers/projectHandlers";
import { createTestingPinia } from "@pinia/testing";

const router = createRouter({
history: createWebHistory(),
Expand All @@ -20,7 +21,18 @@ const router = createRouter({
const renderComponent = () => {
render(CreateProjectButton, {
global: {
plugins: [router, PrimeVue, ToastService]
plugins: [
router,
PrimeVue,
ToastService,
createTestingPinia({
initialState: {
species: {
species: MOCK_SPECIES
}
}
})
]
},
props: {
projects: MOCK_PROJECTS
Expand Down
46 changes: 34 additions & 12 deletions app/client-v2/src/__tests__/stores/projectStore.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
import { getApiUrl } from "@/config";
import { assignResultUri, projectIndexUri, statusUri } from "@/mocks/handlers/projectHandlers";
import { MOCK_PROJECT, MOCK_PROJECT_SAMPLES, MOCK_PROJECT_SAMPLES_BEFORE_RUN } from "@/mocks/mockObjects";
import {
MOCK_PROJECT,
MOCK_PROJECT_SAMPLES,
MOCK_PROJECT_SAMPLES_BEFORE_RUN,
MOCK_SPECIES,
MOCK_SPECIES_CONFIG
} from "@/mocks/mockObjects";
import { server } from "@/mocks/server";
import { useProjectStore } from "@/stores/projectStore";
import { useSpeciesStore } from "@/stores/speciesStore";
import {
type ProjectSample,
AnalysisType,
type AnalysisStatus,
type WorkerResponse,
type HashedFile
type HashedFile,
type ProjectSample,
type WorkerResponse
} from "@/types/projectTypes";
import { flushPromises } from "@vue/test-utils";
import { HttpResponse, http } from "msw";
Expand Down Expand Up @@ -135,6 +142,16 @@ describe("projectStore", () => {

expect(processFilesSpy).toHaveBeenCalledWith([newFileWithHash]);
});
it("should show error toast and not call processFiles when onFilesUpload is called with 0 new files", async () => {
const store = useProjectStore();
store.toast.showErrorToast = vitest.fn();
store.project.samples = mockFilesWithHashes.map((file) => ({ hash: file.hash, filename: file.name }));
const processFilesSpy = vitest.spyOn(store, "processFiles");
store.onFilesUpload(mockFilesWithHashes[0]);

expect(store.toast.showErrorToast).toHaveBeenCalledWith("No new files to upload.");
expect(processFilesSpy).not.toHaveBeenCalled();
});
it("should call batchFilesForProcessing and processFileBatches when processFiles is called", async () => {
const mockHashedFileBatches = [[{ hash: "test-hash" }], [{ hash: "test-hash" }]] as HashedFile[][];
const store = useProjectStore();
Expand Down Expand Up @@ -511,22 +528,27 @@ describe("projectStore", () => {
expect(fileBatches.length).toBe(Math.ceil(mockFilesWithHashes.length / 5));
});

it("should process file batches correctly when processFileBatches is called", async () => {
it("should process file batches correctly with species kmer args when processFileBatches is called", async () => {
const mockFilesWithHashes = Array.from({ length: 98 }, (_, index) => ({
name: `sample${index + 1}.fasta`,
text: () => Promise.resolve(`sample${index + 1}`)
})) as unknown as File[];
const store = useProjectStore();
store.getOptimalWorkerCount = vitest.fn().mockReturnValue(8);
const batchPromise = vitest.spyOn(store, "computeAmrAndSketch").mockResolvedValue();
const hashedFileBatches = await store.batchFilesForProcessing(mockFilesWithHashes);
const speciesStore = useSpeciesStore();
speciesStore.sketchKmerArguments = MOCK_SPECIES_CONFIG;

const projectStore = useProjectStore();
projectStore.project.species = MOCK_SPECIES[0];

projectStore.getOptimalWorkerCount = vitest.fn().mockReturnValue(8);
const batchPromise = vitest.spyOn(projectStore, "computeAmrAndSketch").mockResolvedValue();
const hashedFileBatches = await projectStore.batchFilesForProcessing(mockFilesWithHashes);

await store.processFileBatches(hashedFileBatches);
await projectStore.processFileBatches(hashedFileBatches);
await flushPromises();

expect(store.uploadingPercentage).toEqual(100);
expect(projectStore.uploadingPercentage).toEqual(100);
hashedFileBatches.forEach((batch) => {
expect(batchPromise).toHaveBeenCalledWith(batch);
expect(batchPromise).toHaveBeenCalledWith(batch, MOCK_SPECIES_CONFIG[MOCK_SPECIES[0]]);
});
});
});
Expand Down
69 changes: 69 additions & 0 deletions app/client-v2/src/__tests__/stores/speciesStore.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { speciesConfigIndexUri } from "@/mocks/handlers/configHandlers";
import { MOCK_SPECIES_CONFIG } from "@/mocks/mockObjects";
import { server } from "@/mocks/server";
import { useSpeciesStore } from "@/stores/speciesStore";
import { http, HttpResponse } from "msw";
import { createPinia, setActivePinia } from "pinia";

const mockToastAdd = vitest.fn();
vitest.mock("primevue/usetoast", () => ({
useToast: vitest.fn(() => ({
add: mockToastAdd
}))
}));

describe("SpeciesStore", () => {
beforeEach(() => {
setActivePinia(createPinia());
});

describe("getters", () => {
it("should return correct SketchArguments when getSketchKmerArguments is called", () => {
const store = useSpeciesStore();
store.sketchKmerArguments = MOCK_SPECIES_CONFIG;

const result = store.getSketchKmerArguments("Streptococcus pneumoniae");

expect(result).toEqual(MOCK_SPECIES_CONFIG["Streptococcus pneumoniae"]);
});

it("should return undefined when getSketchKmerArguments is called with an invalid species", () => {
const store = useSpeciesStore();
store.sketchKmerArguments = {
"Streptococcus pneumoniae": {
kmerMax: 14,
kmerMin: 3,
kmerStep: 3
}
} as any;

const result = store.getSketchKmerArguments("Streptococcus agalactiae");

expect(result).toBeUndefined();
});
});
describe("actions", () => {
it("should set speciesConfig from api when setSpeciesConfig is called", async () => {
const store = useSpeciesStore();

await store.setSpeciesConfig();

expect(store.sketchKmerArguments).toEqual(MOCK_SPECIES_CONFIG);
expect(store.species).toEqual(Object.keys(MOCK_SPECIES_CONFIG));
});

it("should show error toast when setSpeciesConfig fails", async () => {
server.use(http.get(speciesConfigIndexUri, () => HttpResponse.error()));
const store = useSpeciesStore();

await store.setSpeciesConfig();

expect(mockToastAdd).toHaveBeenCalledWith({
severity: "error",
detail: "Failed to fetch sketch kmer arguments, please try again later",
life: 5000,
summary: "Error"
});
});
});
});
Loading
Loading