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-188 Allow species selection for project #80

Merged
merged 18 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
a027b77
refactor: Update projectCsvUtils to include species in BaseProjectInfo
absternator Oct 1, 2024
e9723f0
tests for code
absternator Oct 2, 2024
48f0af8
refactor: Remove unused import in CreateProjectButton.vue
absternator Oct 2, 2024
c4fa5b5
refactor: Remove unused import in CreateProjectButton.vue
absternator Oct 2, 2024
cef8963
feat: refactor species to get from api + fix tests
absternator Oct 3, 2024
1526534
tests server
absternator Oct 3, 2024
2add392
refactor: Update CreateProjectButton.vue to use reactive form object
absternator Oct 3, 2024
509378c
Refactor: Update instructions for adding new species
absternator Oct 3, 2024
36d9064
Merge branch 'bacpop-188-mutli-db' of https://github.com/bacpop/beebo…
absternator Oct 3, 2024
a7d8c47
Refactor projectStore.ts and scripts/common
absternator Oct 3, 2024
f6dcea3
Refactor: Rename getSketchKmerArguments to getSpeciesConfig
absternator Oct 7, 2024
5207e74
Refactor: Update script names for downloading databases
absternator Oct 7, 2024
f9022c1
Refactor: Add DBS_LOCATION environment variable to run_dependencies s…
absternator Oct 8, 2024
7e36779
Refactor: Update createProject function in utils.ts to include specie…
absternator Oct 8, 2024
caa4cff
Refactor: Remove unnecessary code in NetworkTab.vue
absternator Oct 8, 2024
25d15f8
Refactor: Update run_dependencies script to include --pull always flag
absternator Oct 8, 2024
7d3a396
Refactor: Update project page to display species name in bold
absternator Oct 9, 2024
e83fa36
Merge pull request #81 from bacpop/bacpop-194-popunk-run
absternator Oct 9, 2024
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*
4 changes: 4 additions & 0 deletions app/client-v2/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,20 @@ declare module 'vue' {
AmrColumn: typeof import('./src/components/ProjectView/AmrColumn.vue')['default']
Button: typeof import('primevue/button')['default']
Column: typeof import('primevue/column')['default']
CreateProjectButton: typeof import('./src/components/HomeView/CreateProjectButton.vue')['default']
CreateProjectDialog: typeof import('./src/components/HomeView/CreateProjectDialog.vue')['default']
CytoscapeCanvas: typeof import('./src/components/ProjectView/CytoscapeCanvas.vue')['default']
DataTable: typeof import('primevue/datatable')['default']
DeleteProjectButton: typeof import('./src/components/HomeView/DeleteProjectButton.vue')['default']
Dialog: typeof import('primevue/dialog')['default']
Dropdown: typeof import('primevue/dropdown')['default']
ExternalLink: typeof import('./src/components/Common/ExternalLink.vue')['default']
FileUpload: typeof import('primevue/fileupload')['default']
InlineMessage: typeof import('primevue/inlinemessage')['default']
InputText: typeof import('primevue/inputtext')['default']
Menu: typeof import('primevue/menu')['default']
Menubar: typeof import('primevue/menubar')['default']
Message: typeof import('primevue/message')['default']
MicroReactColumn: typeof import('./src/components/ProjectView/MicroReactColumn.vue')['default']
MicroReactTokenDialog: typeof import('./src/components/ProjectView/MicroReactTokenDialog.vue')['default']
NetworkGraph: typeof import('./src/components/ProjectView/NetworkGraph.vue')['default']
Expand Down
23 changes: 8 additions & 15 deletions app/client-v2/e2e/home.spec.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,25 @@
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.getByPlaceholder("Create new Project").fill(name);
await page.getByPlaceholder("Create new Project").press("Enter");
await createProject(page, name)

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

await page.getByRole("link", { name: "Beebop home" }).click();
await expect(page.getByRole("link", { name: name })).toBeVisible();
};

test("can add new project from home screen and cannot create duplicate name", async ({ page }) => {
test("can add new project from home screen", async ({ page }) => {
const projectName = randomProjectName();
await addProjectNavigateHome(page, projectName);

await page.getByPlaceholder("Create new Project").fill(projectName);
await page.getByPlaceholder("Create new Project").press("Enter");
await expect(page.getByText("Project name already exists or is empty")).toBeVisible();
});
await addProjectNavigateHome(page, projectName);

test("can not add project with an empty name", async ({ page }) => {
await page.getByPlaceholder("Create new Project").fill("");
await page.getByPlaceholder("Create new Project").press("Enter");
await expect(page.getByText("Project name already exists or is empty")).toBeVisible();
await expect(page.getByText(projectName)).toBeVisible();
});

test("can delete project", async ({ page }) => {
Expand All @@ -45,8 +38,8 @@ test("can edit project name", async ({ page }) => {
await addProjectNavigateHome(page, projectName);

await page.getByLabel("Row Edit").first().click();
await page.getByRole("row", { name: "Save Edit Cancel Edit 0" }).getByRole("textbox").click();
await page.getByRole("row", { name: "Save Edit Cancel Edit 0" }).getByRole("textbox").fill(newProjectName);
await page.getByRole("row", { name: "Save Edit Cancel Edit" }).getByRole("textbox").click();
await page.getByRole("row", { name: "Save Edit Cancel Edit" }).getByRole("textbox").fill(newProjectName);
await page.getByLabel("Save Edit").click();
await expect(page.getByRole("link", { name: newProjectName })).toBeVisible();
});
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
@@ -0,0 +1,136 @@
import { render, screen, waitFor } from "@testing-library/vue";
import CreateProjectButton from "@/components/HomeView/CreateProjectButton.vue";
import { createRouter, createWebHistory } from "vue-router";
import { defineComponent } from "vue";
import PrimeVue from "primevue/config";
import ToastService from "primevue/toastservice";
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(),
routes: [
{ path: "/", component: CreateProjectButton },
{ path: "/project/:id", component: defineComponent({ template: `<div>project page</div>` }) }
]
});
const renderComponent = () => {
render(CreateProjectButton, {
global: {
plugins: [
router,
PrimeVue,
ToastService,
createTestingPinia({
initialState: {
species: {
species: MOCK_SPECIES
}
}
})
]
},
props: {
projects: MOCK_PROJECTS
}
});
};
describe("CreateProject Button component", () => {
it("open modal on create project button click", async () => {
renderComponent();

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

expect(screen.getByText("Create new project for a given species")).toBeVisible();
});

it("should navigate project page on successful project creation", async () => {
const push = vitest.spyOn(router, "push");
renderComponent();

await userEvent.click(screen.getByRole("button", { name: /create project/i }));
await userEvent.click(screen.getByRole("combobox"));
await userEvent.type(screen.getByRole("textbox"), "new project name");
await userEvent.click(screen.getByRole("option", { name: MOCK_PROJECTS[0].species }));

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

await waitFor(() => {
expect(push).toHaveBeenCalledWith(`/project/${MOCK_PROJECTS[0].id}`);
});
});

it("should error if no name or species is selected", async () => {
renderComponent();

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

expect(screen.getByText("Name is required")).toBeVisible();
expect(screen.getByText("Species is required")).toBeVisible();
});

it("should error if duplicate name is created", async () => {
renderComponent();

await userEvent.click(screen.getByRole("button", { name: /create project/i }));
await userEvent.click(screen.getByRole("combobox"));
await userEvent.type(screen.getByRole("textbox"), MOCK_PROJECTS[0].name);
await userEvent.click(screen.getByRole("option", { name: MOCK_PROJECTS[0].species }));

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

expect(screen.getByText("Name already exists")).toBeVisible();
});

it("should not push router if server error on submit", async () => {
const push = vitest.spyOn(router, "push");
server.use(http.post(projectIndexUri, () => HttpResponse.error()));
renderComponent();

await userEvent.click(screen.getByRole("button", { name: /create project/i }));
await userEvent.click(screen.getByRole("combobox"));
await userEvent.type(screen.getByRole("textbox"), "new project name");
await userEvent.click(screen.getByRole("option", { name: MOCK_PROJECTS[0].species }));

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

await waitFor(() => {
expect(push).not.toHaveBeenCalled;
});
});

it("should get rid of error messages on modal close", async () => {
renderComponent();

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

expect(screen.getByText("Name is required")).toBeVisible();
expect(screen.getByText("Species is required")).toBeVisible();

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

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

expect(screen.queryByText("Name is required")).not.toBeInTheDocument();
expect(screen.queryByText("Species is required")).not.toBeInTheDocument();
});

it("should get rid of input fields on cancel button click", async () => {
renderComponent();

await userEvent.click(screen.getByRole("button", { name: /create project/i }));
await userEvent.click(screen.getByRole("combobox"));
await userEvent.type(screen.getByRole("textbox"), "new project name");
await userEvent.click(screen.getByRole("option", { name: MOCK_PROJECTS[0].species }));
await userEvent.click(screen.getByRole("button", { name: "Close" }));

expect(screen.queryByRole("combobox")).not.toBeInTheDocument();
expect(screen.queryByRole("textbox")).not.toBeInTheDocument;
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import ProjectDataTableVue from "@/components/ProjectView/ProjectDataTable.vue";
import { MOCK_PROJECT_SAMPLES, MOCK_PROJECT_SAMPLES_BEFORE_RUN } from "@/mocks/mockObjects";
import { 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";
Expand Down
Loading
Loading